This commit is contained in:
Zeeshaun
2026-03-17 08:54:34 -05:00
parent 64856c640e
commit cdcba54aa4
330 changed files with 54110 additions and 0 deletions
@@ -0,0 +1,17 @@
**/*.bat
**/*.log
**/*.md
**/*.yml
**/.dockerignore
**/.editorconfig
**/.git
**/.gitattributes
**/.gitignore
**/.vs
**/.vscode
**/bin
**/dist
**/dockerfile
**/node_modules
**/obj
**/package-lock.json
@@ -0,0 +1,334 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# SpecFlow pickled files
Specs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
*.feature.cs
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
@@ -0,0 +1,51 @@
# .NET Core SDK
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS aspnetcore-build
# Copy Projects (context is Api/)
COPY ReserveManagementSystemApi/ReserveManagementSystemApi.csproj ./source/ReserveManagementSystemApi/
COPY ReserveManagementSystemLib/ReserveManagementSystemLib.csproj ./source/ReserveManagementSystemLib/
# Install Credential Provider and set env variables to enable Nuget restore with auth
ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \
update-ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Download the NuGet Credential Provider directly (Docker's ADD uses trusted TLS)
ADD https://github.com/microsoft/artifacts-credprovider/releases/latest/download/Microsoft.Net6.NuGet.CredentialProvider.tar.gz /tmp/
# Extract and install Credential Provider
RUN mkdir -p /root/.nuget/plugins/netcore && \
tar --strip-components=3 -xzf /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz -C /root/.nuget/plugins/netcore && \
rm /tmp/Microsoft.Net6.NuGet.CredentialProvider.tar.gz
ENV NUGET_PLUGIN_PATHS=/root/.nuget/plugins/netcore/CredentialProvider.Microsoft.dll
ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED=true
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}"
# .NET Core Restore, use auth'd feed
RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj
# Copy All Files
COPY . ./source/
FROM aspnetcore-build AS test
LABEL test=true
COPY ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj ./source/ReserveManagementSystemApi.Test/
RUN dotnet restore ./source/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj
RUN dotnet test -c Release --results-directory /testresults --logger "trx;LogFileName=test_results.trx" ./source/ReserveManagementSystemApi.Test/ReserveManagementSystemApi.Test.csproj
# .NET Core Publish
FROM aspnetcore-build AS publish
RUN dotnet publish ./source/ReserveManagementSystemApi/ReserveManagementSystemApi.csproj -c Release -o /dist
# ASP.NET Core
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine3.14
RUN apk add --no-cache icu-libs && apk add --no-cache tzdata;
ENV TZ=America/Chicago
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV ASPNETCORE_ENVIRONMENT=Development
WORKDIR /app
COPY --from=publish /dist .
ENTRYPOINT ["dotnet", "ReserveManagementSystemApi.dll"]
@@ -0,0 +1,9 @@
CREATE TABLE [dbo].[Dealer]
(
[DealerCode] VARCHAR(4) NULL,
[Name] VARCHAR(40) NULL,
[DealerAccountNumber] VARCHAR(20) NULL,
[DealerOrganizationNumber] VARCHAR(20) NOT NULL,
[DealerGroupCode] VARCHAR(10) NULL,
CONSTRAINT [PK_Dealer] PRIMARY KEY (DealerOrganizationNumber)
)
@@ -0,0 +1,17 @@
/*
Do not change the database path or name variables.
Any sqlcmd variables will be properly substituted during
build and deployment.
*/
CREATE TABLE [dbo].[Import](
[ImportId] int NOT NULL IDENTITY,
[StartDate] DATETIME2 NOT NULL,
[EndDate] DATETIME2 NULL,
[RecordCount] [int] NOT NULL DEFAULT 0,
[RecordsSaved] [int] NOT NULL DEFAULT 0,
[Success] [bit] NOT NULL,
[FileName] [varchar](50) NULL,
[ImportType] [varchar](10) NULL
CONSTRAINT [PK_Import] PRIMARY KEY (ImportId)
)
@@ -0,0 +1,41 @@
CREATE TABLE [dbo].[Loan](
[LoanId] int NOT NULL IDENTITY,
[ImportId] [int] NOT NULL,
[MemberNumber] [varchar](13) NULL,
[LoanNumber] [varchar](20) NOT NULL,
[MemberLastName] [varchar](60) NULL,
[MemberFirstName] [varchar](20) NULL,
[MemberInitial] [char](1) NULL,
[DealerOrganizationNumber] [varchar](20) NOT NULL,
[LoanDate] [smalldatetime] NOT NULL,
[FirstPmtdate] [smalldatetime] NOT NULL,
[FinalPmtDate] [smalldatetime] NOT NULL,
[PostedDate] [smalldatetime] NOT NULL,
[LoanAmount] [money] NOT NULL,
[BuyRate] [decimal](5, 3) NOT NULL,
[ContractRate] [decimal](5, 3) NOT NULL,
[MonthlyPmt] [decimal](20, 4) NOT NULL,
[LoanTypeId] [char](2) NULL,
[Term] [int] NOT NULL DEFAULT 0,
[ReserveAmount] [money] NOT NULL DEFAULT 0,
[FlatFeePaid] [bit] NOT NULL,
[Chargeback] [money] NULL,
[PaidLoanDate] [smalldatetime] NULL,
[ReserveMessage] [varchar](100) NOT NULL,
[ChargebackMessage] [varchar](100) NULL,
[ActionRequired] [bit] NOT NULL DEFAULT 0,
[StatementDate] [smalldatetime] NULL,
[Adjustment] [money] NULL,
[LoanStatus] [varchar](25) NULL,
[ExportedReserveDate] [datetime2] NULL,
[ExportedChargebackDate] [datetime2] NULL,
[ApplicationId] VARCHAR(20) NULL,
[Warranty] BIT NOT NULL DEFAULT 0,
[GAP] BIT NOT NULL DEFAULT 0,
[CreditLife] BIT NOT NULL DEFAULT 0,
[CreditDisability] BIT NOT NULL DEFAULT 0,
[MBP] BIT NOT NULL DEFAULT 0,
[RemainingMonthsAtClose] TINYINT NULL,
[AccountType] VARCHAR(4) NULL,
CONSTRAINT [PK_Loan] PRIMARY KEY (LoanId)
)
@@ -0,0 +1,13 @@
CREATE TABLE [dbo].[LoanAudit](
[AuditId] int NOT NULL IDENTITY,
[LoanId] int NULL,
[MemberNumber] VARCHAR(13) NULL,
[LoanNumber] VARCHAR(20) NULL,
[ModifiedBy] [varchar](50) NOT NULL,
[ModifiedDate] [smalldatetime] NOT NULL,
[OldValue] [varchar](MAX) NULL,
[NewValue] [varchar](MAX) NULL,
[ActivityType][varchar](20) NULL,
[AuditDescription] [varchar] (255) NULL,
CONSTRAINT [PK_LoanAudit] PRIMARY KEY (AuditId),
)
@@ -0,0 +1,11 @@
CREATE TABLE [dbo].[LoanType](
[LoanTypeId] [char](2) NOT NULL,
[LoanComment] [varchar](70) NOT NULL,
[ChargebackMethod] [char](1) NOT NULL,
[NoChargebackDays] [smallint] NOT NULL,
[NoChargebackPayments] [smallint] NOT NULL,
[ReserveDiscount] [decimal](4,2) NOT NULL,
[FlatFeePercent] [decimal](4,2) NOT NULL,
[NoReserveThreshold] [decimal](4,2) NOT NULL,
CONSTRAINT [PK_LoanType] PRIMARY KEY ([LoanTypeId])
)
@@ -0,0 +1,11 @@
/*
Do not change the database path or name variables.
Any sqlcmd variables will be properly substituted during
build and deployment.
*/
CREATE TABLE [dbo].[MinorAccountType](
[MinorAccountTypeId] int NOT NULL IDENTITY,
[AccountType] [varchar](4) NULL,
[GeneralLedgerAccountNumber] [varchar](20) NULL
CONSTRAINT [PK_MinorAccountType] PRIMARY KEY (MinorAccountTypeId)
)
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Operations Version="1.0" xmlns="http://schemas.microsoft.com/sqlserver/dac/Serialization/2012/02">
<Operation Name="Rename Refactor" Key="69fc61bd-1b0d-4310-ab5f-4e3fccb9c3df" ChangeDateTime="05/13/2021 15:21:43">
<Property Name="ElementName" Value="[dbo].[Dealer].[Id]" />
<Property Name="ElementType" Value="SqlSimpleColumn" />
<Property Name="ParentElementName" Value="[dbo].[Dealer]" />
<Property Name="ParentElementType" Value="SqlTable" />
<Property Name="NewName" Value="DealerCode" />
</Operation>
<Operation Name="Rename Refactor" Key="0a110887-6676-4aca-9942-f330f30f704f" ChangeDateTime="05/19/2021 21:46:24">
<Property Name="ElementName" Value="[dbo].[Loan].[LoanType]" />
<Property Name="ElementType" Value="SqlSimpleColumn" />
<Property Name="ParentElementName" Value="[dbo].[Loan]" />
<Property Name="ParentElementType" Value="SqlTable" />
<Property Name="NewName" Value="LoanTypeId" />
</Operation>
<Operation Name="Rename Refactor" Key="23eba713-18fa-44bb-a71e-04e955a9b20a" ChangeDateTime="08/13/2021 21:40:09">
<Property Name="ElementName" Value="[dbo].[Dealer].[DealerGroup]" />
<Property Name="ElementType" Value="SqlSimpleColumn" />
<Property Name="ParentElementName" Value="[dbo].[Dealer]" />
<Property Name="ParentElementType" Value="SqlTable" />
<Property Name="NewName" Value="DealerGroupCode" />
</Operation>
<Operation Name="Rename Refactor" Key="e3d400fd-ceec-4ff6-9557-c2403be5fd00" ChangeDateTime="08/13/2021 21:52:38">
<Property Name="ElementName" Value="[dbo].[Loan].[DealerCode]" />
<Property Name="ElementType" Value="SqlSimpleColumn" />
<Property Name="ParentElementName" Value="[dbo].[Loan]" />
<Property Name="ParentElementType" Value="SqlTable" />
<Property Name="NewName" Value="DealerOrg" />
</Operation>
<Operation Name="Rename Refactor" Key="79da7c58-8312-4cea-b111-4ff27f87ad19" ChangeDateTime="08/13/2021 21:53:00">
<Property Name="ElementName" Value="[dbo].[Loan].[DealerOrg]" />
<Property Name="ElementType" Value="SqlSimpleColumn" />
<Property Name="ParentElementName" Value="[dbo].[Loan]" />
<Property Name="ParentElementType" Value="SqlTable" />
<Property Name="NewName" Value="DealerOrganizationNumber" />
</Operation>
</Operations>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<Name>ReserveManagementSystemApi.Sql</Name>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>4.1</ProjectVersion>
<ProjectGuid>{5a41c8dc-6f4d-4fd2-b03d-d0b2d8b363df}</ProjectGuid>
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider</DSP>
<OutputType>Database</OutputType>
<RootPath>
</RootPath>
<RootNamespace>ReserveManagementSystemApi.Sql</RootNamespace>
<AssemblyName>ReserveManagementSystemApi.Sql</AssemblyName>
<ModelCollation>1033, CI</ModelCollation>
<DefaultFileStructure>BySchemaAndSchemaType</DefaultFileStructure>
<DeployToDatabase>True</DeployToDatabase>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetLanguage>CS</TargetLanguage>
<AppDesignerFolder>Properties</AppDesignerFolder>
<SqlServerVerification>False</SqlServerVerification>
<IncludeCompositeObjects>True</IncludeCompositeObjects>
<TargetDatabaseSet>True</TargetDatabaseSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>bin\Debug\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<!-- Default to the v11.0 targets path if the targets file for the current VS version is not found -->
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
</PropertyGroup>
<Import Condition="'$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<Import Condition="'$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<ItemGroup>
<Folder Include="Properties" />
</ItemGroup>
<ItemGroup>
<Build Include="Loan.sql" />
<Build Include="LoanAudit.sql" />
<Build Include="LoanType.sql" />
<Build Include="Import.sql" />
<Build Include="Dealer.sql" />
<Build Include="Vehicle.sql" />
<Build Include="MinorAccountType.sql" />
</ItemGroup>
<ItemGroup>
<None Include="ReserveManagementSystem.publish.xml" />
</ItemGroup>
<ItemGroup>
<PostDeploy Include="Script.PostDeploymentPopulateLoanTypeTable.sql" />
</ItemGroup>
<ItemGroup>
<PreDeploy Include="Script.PreDeploymentTruncateLoanTypes.sql" />
</ItemGroup>
<ItemGroup>
<RefactorLog Include="ReserveManagementSystemApi.Sql.refactorlog" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
/*
Post-Deployment Script Template
--------------------------------------------------------------------------------------
This file contains SQL statements that will be appended to the build script.
Use SQLCMD syntax to include a file in the post-deployment script.
Example: :r .\myfile.sql
Use SQLCMD syntax to reference a variable in the post-deployment script.
Example: :setvar TableName MyTable
SELECT * FROM [$(TableName)]
--------------------------------------------------------------------------------------
*/
USE [ReserveManagementSystem]
GO
INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('BB', 'Standard Loan', 'S', 0, 0, 0.00, 1.00, 0.25)
INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('NC', '75% Reserve Paid - No charge back- after 90 Days', 'N', 90, 3, 25.00, 1.00, 0.25)
INSERT INTO [dbo].[LoanType] (LoanTypeId, LoanComment, ChargebackMethod, NoChargebackDays, NoChargebackPayments, ReserveDiscount, FlatFeePercent, NoReserveThreshold) VALUES ('NR', 'No reserve to be paid', 'N', 0, 0, 0.00, 0.00, 0.25)
@@ -0,0 +1,6 @@
USE [ReserveManagementSystem]
GO
IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'LoanType')
BEGIN
TRUNCATE TABLE [dbo].[LoanType];
END
@@ -0,0 +1,12 @@
CREATE TABLE [dbo].[Vehicle]
(
[VehicleId] INT NOT NULL IDENTITY,
[LoanId] INT NULL,
[VINNumber] VARCHAR(30) NULL,
[Make] VARCHAR(30) NULL,
[Model] VARCHAR(30) NULL,
[Year] VARCHAR(4) NULL,
[Mileage] FLOAT NULL,
[Color] VARCHAR(30) NULL,
CONSTRAINT [PK_Vehicle] PRIMARY KEY (VehicleId)
)
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.12" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReserveManagementSystemApi\ReserveManagementSystemApi.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,780 @@
using Moq;
using Microsoft.EntityFrameworkCore;
using Xunit;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemApi.Mappers;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
using ReserveManagementSystemLib.Persistence;
namespace ReserveManagementSystemApi.Test.Service
{
public class ExportServiceTest
{
private ExportService _exportService;
private Mock<ICoreApiServiceClient> _mockCoreApiServiceClient;
private Mock<IMinorAccountTypeRepository> _mockMinorAccountRepository;
private Mock<RmsApiConfiguration> _mockConfiguration;
private AppDbContext _appDbContext;
private ILoanRepository _loanRepository;
public ExportServiceTest()
{
_mockCoreApiServiceClient = new Mock<ICoreApiServiceClient>();
_mockMinorAccountRepository = new Mock<IMinorAccountTypeRepository>();
_mockConfiguration = new Mock<RmsApiConfiguration>();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
_loanRepository = new LoanRepository(_appDbContext);
_mockConfiguration.Object.DefaultBatchSize = 100;
_mockConfiguration.Object.DefaultImportStartDate = "2021-07-01 00:00:00";
_mockConfiguration.Object.DefaultConvertedLoanPostDate = "2021-10-31";
_exportService = new ExportService(_loanRepository, _mockCoreApiServiceClient.Object, _mockMinorAccountRepository.Object, _mockConfiguration.Object);
}
[Fact]
public async Task SendExportChargebacksAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedChargebackDate = null;
loan.PaidLoanDate = Convert.ToDateTime("2021-11-01");
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ChargeBack = (decimal)22.00;
var account = new MinorAccountType()
{
MinorAccountTypeId = 1,
GeneralLedgerAccountNumber = "11111111",
AccountType = "DL01"
};
_loanRepository.Add(loan);
_mockMinorAccountRepository.Setup(repo => repo.GetMinorAccountType("DL01")).Returns(account);
var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var response = new MonetaryTransactionResponse()
{
WasSuccessful = true
};
var listResponse = new List<MonetaryTransactionResponse> { response };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(batchList)).Returns(Task.FromResult(listResponse)).Verifiable();
var numberOfLoansUpdatedSuccessfully = await _exportService.SendBatchListMonetaryTransactionRequests(groupedList);
var success = numberOfLoansUpdatedSuccessfully[0].WasSuccessful.ToString();
Assert.True(true, success);
}
#region "Unit tests for Chargeback rounding"
[Fact]
public async Task MappedExportRoundedUpChargebackAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var account = new MinorAccountType()
{
MinorAccountTypeId = 1,
GeneralLedgerAccountNumber = "11111111",
AccountType = "DL01"
};
loan.Adjustment = 0;
loan.ChargeBack = (decimal)502.525;
var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
Assert.Equal(-502.53, batchList[0].MonetaryTransactionRequests[1].TransactionAmount);
Assert.Equal("ChrgBk-1111100000", batchList[0].MonetaryTransactionRequests[1].Description);
}
[Fact]
public async Task MappedExportRoundedDownChargebackAsyncTest_OK()
{
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "2411000009999";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var account = new MinorAccountType()
{
MinorAccountTypeId = 1,
GeneralLedgerAccountNumber = "11111111",
AccountType = "DL01"
};
loan.Adjustment = 0;
loan.ChargeBack = (decimal)38.171850;
var result = ExportMapper.MapLoanToIndirectLoanForChargeback(loan, account);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
Assert.Equal(-38.17, batchList[0].MonetaryTransactionRequests[1].TransactionAmount);
Assert.Equal("ChrgBk-2411000009999", batchList[0].MonetaryTransactionRequests[1].Description);
}
}
#endregion
#region "Unit tests for Reserve rounding"
[Fact]
public void MappedExportRoundedUpReserveAsyncTest1_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)502.525;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-502.53, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public void MappedExportRoundedUpReserveAsyncTest2_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1237.715;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1237.72, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpReserveAsyncTest3_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1439.675;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1439.68, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownReserveAsyncTest1_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1982.1045;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1982.10, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownReserveAsyncTest2_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1803.52449;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1803.52, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownReserveAsyncTest3_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)38.171850;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-38.17, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest1_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)4240.9217;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-4240.92, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest2_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)397.2345;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-397.23, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest3_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)434.9949;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-434.99, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest4_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1513.1451;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1513.15, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAsyncTest5_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)4240.9247;
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-4240.92, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpTenThousandthReserveAsyncTest1_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
loan.Adjustment = 0;
loan.ReserveAmount = (decimal)1513.154999; //value that would be written to DB is 1513.155
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1513.16, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
#endregion
#region "Unit tests for reserve rounding with adjustments"
[Fact]
public async Task MappedExportRoundedDownReserveAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(1513.154999 - 1803.4656); //1513.16 - 1803.47
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)1513.155; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(290.31, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedDownTenThousandthReserveAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(1513.1489 - 1803.4734); //-290.3245
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)1513.1489; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(290.32, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpHundredThousandthReserveAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(1513.145 - 1803.52449); //-290.37949 //Reserve written to DB 1803.5245
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)1513.145; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(290.38, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpMillionthReserveNegativeOneAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(39.171450 - 38.171550); //0.999899999999997
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)38.1715; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(-1, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpMillionthReservePositiveOneAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(38.171550 - 39.171450); //-0.999899999999997
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)38.1715; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(1, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
[Fact]
public async Task MappedExportRoundedUpMillionthReserveNoAdjustmentAsyncTest_OK()
{
var loan = new Loan();
loan.ExportedReserveDate = null;
loan.ExportedChargebackDate = null;
loan.LoanNumber = "1111100000";
loan.LoanId = 1;
loan.DealerOrganizationNumber = "15500";
loan.AccountType = "DL01";
loan.Dealer = new Dealer()
{
DealerAccountNumber = "7788888",
DealerOrganizationNumber = "15500"
};
var adjustmentCalculation = (decimal)(38.171450 - 38.171850); //-0.000399999999999068
loan.Adjustment = (decimal)adjustmentCalculation;
loan.ReserveAmount = (decimal)38.1719; //as saved in DB
var result = ExportMapper.MapLoanToIndirectLoanForReserve(loan);
var groupedList = new List<GroupedMonetaryTransactionRequest> { result };
var resultBatch = BatchHelper.CreateBatch(groupedList, _mockConfiguration.Object.DefaultBatchSize);
List<GroupedMonetaryTransactionRequest> batchList = new List<GroupedMonetaryTransactionRequest>(resultBatch.FirstOrDefault());
//in order to test adjustment to the reserve rounding set the reserves and adjustments accordingly.
Assert.Equal(0, batchList[0].MonetaryTransactionRequests[0].TransactionAmount);
}
#endregion
}
}
@@ -0,0 +1,631 @@
Feature: Chargeback Calculation
Calculate chargeback for a paid loan
Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 2/10/2021 | 3/12/2021 | 2/12/2028 | NC | 1206.8106 | 2/19/2021 | 84 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1206.8106 | Paid On or Before Chargeback Date | 84 |
#May 11, 2021 vs. May 12, 2021 First Payment Date wins by one day
Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment, Compare Loan Date vs. First Pmt Date, First Pmt Date Wins by 1 day
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 2/10/2021 | 3/12/2021 | 2/12/2028 | NC | 1206.8106 | 2/19/2021 | 84 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1206.8106 | Paid On or Before Chargeback Date | 84 |
Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 2/1/2021 | 2/15/2021 | 2/15/2028 | NC | 1206.8106 | 2/19/2021 | 85 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1178.7452372093023255813953488 | Paid On or Before Chargeback Date | 84 |
Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, in a different year
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/1/2020 | 12/15/2020 | 12/15/2027 | NC | 1206.8106 | 1/19/2021 | 85 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1151.0100551573187414500683995 | Paid On or Before Chargeback Date | 83 |
Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 2/10/2021 | 3/10/2021 | 2/10/2028 | NC | 1206.8106 | 2/19/2021 | 84 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1206.8106 | Paid On or Before Chargeback Date | 84 |
Scenario: NC - Calculate Chargeback for Paid Date is after 15 days of Loan date, Compare Loan Date vs. First Pmt Date, Loan Date Wins by 1 day, Leslie's scenario
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/15/2019 | 2/14/2019 | 4/14/2025 | NC | 2141.8721 | 4/15/2019 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1975.0315364210526315789473684 | Paid On or Before Chargeback Date | 72 |
Scenario: Calculate Chargeback for Paid Date is within 15 days of Loan date, 30 days to first payment - Missing Loan Type Id
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 2/10/2021 | 3/12/2021 | 2/12/2028 | | 1206.8106 | 2/19/2021 | 84 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1206.8106 | Paid On or Before Chargeback Date | 84 |
Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 45 days to first payment
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/30/2017 | 12/14/2017 | 11/14/2023 | NC | 927.0989 | 11/6/2017 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 927.0989 | Paid On or Before Chargeback Date | 72 |
Scenario: NC - Calculate Chargeback for Paid Date is within 15 days of Loan date, 90 days to first payment
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/7/2020 | 1/5/2021 | 12/5/2025 | NC | 229.0637 | 10/21/2020 | 48 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 238.60802083333333333333333333 | Paid On or Before Chargeback Date | 49 |
Scenario: NC - Calculate Chargeback for Paid Date is exactly 15 days from Loan date, 45 days to first payment
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/7/2020 | 1/21/2021 | 12/21/2025 | NC | 878.8602 | 12/22/2020 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 878.8602 | Paid On or Before Chargeback Date | 60 |
Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 32 days until first payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/28/2021 | 3/1/2021 | 5/1/2027 | NC | 855.6368 | 2/25/2021 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 833.1200421052631578947368421 | Paid On or Before Chargeback Date | 74 |
Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 45 days until first payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | NC | 1320.8907 | 10/31/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1320.8907 | Paid On or Before Chargeback Date | 72 |
Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 90 days until first payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/22/2020 | 3/22/2021 | 2/22/2027 | NC | 1689.8999 | 1/14/2021 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1736.8415638888888888888888889 | Paid On or Before Chargeback Date | 73 |
Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date but before or on the first payment date, 91 days until first payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 11/15/2018 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 209.36388333333333333333333333 | Paid On or Before Chargeback Date | 73 |
Scenario: NC - Calculate Chargeback for Paid Date more than 15 days past Loan date after the first payment date, over Feb month example to trigger "not a date" condition.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/11/2021 | 4/25/2021 | 3/25/2027 | NC | 309.4481 | 4/30/2021 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 300.9700698630136986301369863 | Paid On or Before Chargeback Date | 71 |
Scenario: NC - Calculate Chargeback for Paid Date past the chargeback date - this is day after
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 3/26/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 69 |
Scenario: NC - Calculate Chargeback for Paid Date past the final payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/26/2018 | 1/25/2019 | 12/25/2024 | NC | 203.7054 | 3/26/2025 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 0 |
Scenario: NR - Calculate Chargeback for NR loan - paid past chargeback date (month before first payment date)
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 10/31/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | No Reserve - No Chargeback | 72 |
Scenario: NR - Calculate Chargeback for NR loan - paid before chargeback date, 0 chargeback because reserve is 0
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 10/20/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | No Reserve - No Chargeback | 72 |
Scenario: NR - Calculate Chargeback for NR loan - paid after final payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | NR | 0 | 12/1/2025 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | No Reserve - No Chargeback | 0 |
Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because reserve is 0
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | BB | 0 | 10/20/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | No Reserve - No Chargeback | 72 |
Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because it is paid after final payment date.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 8/24/2017 | 9/23/2017 | 8/23/2020 | BB | 127.24 | 5/18/2021 | 36 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 0 |
Scenario: BB - Calculate Chargeback for BB loan - 0 chargeback because it is paid on the final payment date.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 8/24/2017 | 9/23/2017 | 5/18/2021 | BB | 127.24 | 5/18/2021 | 36 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 0 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, past chargeback date, NoChargebackPayments
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 9/11/2018 | 10/24/2018 | 12/24/2024 | BB | 287.3639 | 4/30/2021 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 99.82114421052631578947368421 | Paid On or Before Chargeback Date | 44 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid before first payment date, NoChargebackPayments
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/15/2019 | 11/29/2019 | 10/29/2025 | BB | 500 | 10/20/2019 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 500 | Paid On or Before Chargeback Date | 72 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid more than a year after first payment.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/18/2018 | 3/4/2018 | 2/4/2023 | BB | 790.4206 | 4/30/2021 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 99.77440360655737704918032787 | Paid On or Before Chargeback Date | 21 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, almost to final payment date.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 5/2/2015 | 6/10/2015 | 5/10/2021 | BB | 26938.75 | 4/30/2021 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 0 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, before final payment date.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/30/2017 | 5/14/2017 | 4/14/2022 | BB | 115.00 | 4/30/2021 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 4.1475409836065573770491803279 | Paid On or Before Chargeback Date | 11 |
Scenario: BB - Calculate Chargeback for BB loan - using BB Chargeback method, paid within a year of loan date.
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/22/2020 | 2/5/2021 | 4/5/2027 | BB | 265.9569 | 4/26/2021 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 238.52134610526315789473684211 | Paid On or Before Chargeback Date | 71 |
Scenario: BB - Calculate Chargeback for BB loan - paid after final pmt date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/22/2020 | 2/5/2021 | 4/5/2027 | BB | 265.9569 | 4/26/2027 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard Loan | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 0 |
Scenario: BB - Calculate Chargeback for Buydown reserve - checking Buydown message buydown reserve below threshold
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage |
| 8/4/2017 | 9/3/2017 | 8/3/2022 | NC | 0 | 10/20/2019 | 60 | BuyDown - Reserve - Below Threshold |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Buy Down Reserve - No Chargeback | 33 |
Scenario: NC - Calculate Chargeback for Buydown reserve - checking Buydown message buydown reserve and negative value
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage |
| 9/30/2019 | 11/14/2019 | 10/14/2025 | NC | -3471.1087 | 3/20/2021 | 72 | BuyDown - Reserve |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Buy Down Reserve - No Chargeback | 55 |
Scenario: NC - Calculate Boat Chargeback for Buydown reserve - checking Buydown message buydown reserve and negative value
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | ReserveMessage | AccountType |
| 9/30/2019 | 11/14/2019 | 10/14/2025 | NC | -3471.1087 | 3/20/2021 | 60 | BuyDown - Reserve | DL10 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve Paid - No charge back- after 90 Days | N | 90 | 3 | 25 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Buy Down Reserve - No Chargeback | 43 |
Scenario: NC - Calculate Boat Chargeback for paid before chargeback date - Term 63 or less
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType |
| 7/4/2019 | 8/3/2019 | 10/3/2024 | NC | 460.04 | 11/21/2019 | 63 | DL12 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 390.44069444444444444444444444 | Paid On or Before Chargeback Date | 58 |
Scenario: NC - Calculate Boat Chargeback for paid before chargeback date - Term greater than 63
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType |
| 7/3/2019 | 8/2/2019 | 7/2/2029 | NC | 1163.05 | 5/6/2020 | 120 | DL11 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 978.0193181818181818181818182 | Paid On or Before Chargeback Date | 110 |
Scenario: NC - Calculate Boat Chargeback for after chargeback date - Term 63 or less
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType |
| 07/04/2019 | 8/3/2019 | 10/3/2024 | NC | 460.04 | 12/21/2020 | 63 | DL11 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 45 |
Scenario: NC - Calculate Boat Chargeback for after chargeback date - Term greater than 63
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term | AccountType |
| 7/3/2019 | 8/2/2019 | 7/2/2029 | NC | 1163.05 | 5/6/2021 | 120 | DL09 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 0 | Paid After Chargeback Date | 98 |
Scenario Outline: NC - Calculate Chargeback for paid date between first payment date and chargeback date examples 1 and 2
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/26/2020 | 2/9/2021 | 4/9/2027 | NC | 1815.0503 | <PaidLoanDate> | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| <Chargeback> | <ChargebackMessage> | <RemainingMonthsAtClose> |
Examples:
| PaidLoanDate | Chargeback | ChargebackMessage | RemainingMonthsAtClose |
| 2/26/2021 | 1720.1581965964912280701754386 | Paid On or Before Chargeback Date | 73 |
| 2/25/2021 | 1720.1581965964912280701754386 | Paid On or Before Chargeback Date | 73 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 3
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/29/2020 | 2/12/2021 | 1/12/2027 | NC | 1835.6378 | 2/26/2021 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 1785.3463534246575342465753425 | Paid On or Before Chargeback Date | 71 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 4
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 11/23/2020 | 1/7/2021 |12/7/2025 | NC | 1085.4654 | 2/25/2021 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 980.477762950819672131147541 | Paid On or Before Chargeback Date | 57 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 5
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/14/2020 | 1/28/2021 | 12/28/2024 | NC | 58.5443 | 2/19/2021 | 48 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 53.814956037414965986394557823 | Paid On or Before Chargeback Date | 46 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 6
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/22/2020 | 2/5/2021 | 1/5/2027 | NC | 853.1331 | 2/19/2021 | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 829.7595904109589041095890411 | Paid On or Before Chargeback Date | 71 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 7
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 12/4/2020 | 1/18/2021 | 12/18/2025 | NC | 584.2477 | 2/18/2021 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 546.25563644808743169398907104 | Paid On or Before Chargeback Date | 58 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 8 - paid day after first payment date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/2/2021 | 2/16/2021 | 11/16/2024 | NC | 116.0986 | 2/17/2021 | 46 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 111.15823404255319148936170213 | Paid On or Before Chargeback Date | 45 |
Scenario: NC - Calculate Chargeback for paid date between first payment date and chargeback date - example 9 - first payment in a month with 30 days
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 10/14/2020 | 11/28/2020 | 10/28/2023 | NC | 110.7846 | 1/25/2021 | 36 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 93.31855945945945945945945946 | Paid On or Before Chargeback Date | 33 |
Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 29th, not leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/16/2020 | 4/15/2020 | 3/15/2025 | NC | 169.0256 | 4/29/2020 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 163.48377704918032786885245902 | Paid On or Before Chargeback Date | 59 |
Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 30th, not leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/17/2017 | 4/16/2017 | 3/16/2019 | NC | 86.4577 | 5/30/2017 | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 |
Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 31st, not leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/17/2017 | 4/16/2017 | 3/16/2019 | NC | 86.4577 | 5/31/2017 | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 |
Scenario: NC - Calculate Chargeback using Arbitrary Date (paid loan date 29th, leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 1/16/2019 | 4/15/2019 | 3/15/2024 | NC | 169.0256 | 4/29/2019 | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 163.48377704918032786885245902 | Paid On or Before Chargeback Date | 59 |
Scenario: NC - Calculate Chargeback using Arbitrary Date (paid loan date 30th, leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/17/2018 | 4/16/2018 | 3/16/2020 | NC | 86.4577 | 5/30/2018 | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 |
Scenario: NC - Calculate Chargeback without using Arbitrary Date (paid loan date 31st, leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 3/17/2018 | 4/16/2018 | 3/16/2020 | NC | 86.4577 | 5/31/2018 | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 72.912660333333333333333333333 | Paid On or Before Chargeback Date | 22 |
Scenario: NC - Calculate Chargeback using Arbitrary Date with Final pmt after Arbitrary Date (paid loan date 31st, not leap year) for Paid Date after First Payment Date
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Type Id | ReserveAmount | PaidLoanDate | Term |
| 11/30/2018 | 1/1/2019 | 3/1/2025 | NC | 2456.5546 | 1/31/2019 | 75 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the chargeback is calculated
Then the chargeback returned is
| ChargebackAmount | ChargebackTypeMessage | RemainingMonthsAtClose |
| 2328.1242016140350877192982456 | Paid On or Before Chargeback Date | 73 |
@@ -0,0 +1,155 @@
Feature: Dealer Service
Get a list of dealers when searching code or name. Update dealers
Scenario: Search for Dealer - Code - find multiple dealers
Given the dealer table looks like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
When I search for dealers with search string 'S1'
Then dealers returned should be
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 |
Scenario: Search for Dealer - Code - no dealers returned
Given the dealer table looks like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
When I search for dealers with search string 'A1'
Then dealers returned should be
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
Scenario: Search for Dealer - Name - find multiple dealers
Given the dealer table looks like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| S11 | ANTHONY AUTO GROUP OF WEST BEND | 5678 | 7535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
When I search for dealers with search string 'WEST'
Then dealers returned should be
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S11 | ANTHONY AUTO GROUP OF WEST BEND | 5678 | 7535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
Scenario: Search for Dealer - Name - no dealers returned
Given the dealer table looks like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| S11 | ANTHONY AUTO GROUP | 5678 | 7535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
When I search for dealers with search string 'A1'
Then dealers returned should be
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
Scenario: Search for Dealer - Code and name - find multiple dealers
Given the dealer table looks like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| A1 | ANTHONY AUTO GROUP OF WEST BEND S1 | 9234 | 3535434333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | 9535434333 |
When I search for dealers with search string 'S1'
Then dealers returned should be
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 | 4535434333 |
| A1 | ANTHONY AUTO GROUP OF WEST BEND S1 | 9234 | 3535434333 |
Scenario: Update dealers from loans - add dealer that doesn't exist
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 |
And the dealer table looks like
|Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 |
Scenario: Update dealers from loans - add dealer with blank org number
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | | | | |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434333 |
| | | | |
Scenario: Update dealers from loans - update name and account number
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 |
Scenario: Update dealers from loans - DO update dealer code
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S11 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S11 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
Scenario: Update dealers from loans - only take updates from the first loan with that dealer
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 |
| 1 | 1234 | S1 | BEST SUBURBAN MOTORS HD | 4535434335 |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HD | 4535434334 |
Scenario: Update dealers from loans - a null dealer code doesn't get pulled in
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | <null> | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
Scenario: Update dealers from loans - a null dealer name doesn't get pulled in
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S1 | <null> | 4535434334 |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
Scenario: Update dealers from loans - a null dealer account number doesn't get pulled in
Given a list of loans with dealers that look like
| LoanId | Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1 | 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | <null> |
And the dealer table looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
When I update the dealers from the loans
Then the dealer table now looks like
| Dealer Organization Number | Dealer Code | Name | Dealer Account Number |
| 1234 | S1 | SUBURBAN MOTORS HARLEY DAVIDSON | 4535434334 |
@@ -0,0 +1,95 @@
using Microsoft.EntityFrameworkCore;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using TechTalk.SpecFlow.Assist.ValueRetrievers;
namespace ReserveManagementSystemApi.Tests.Features
{
[Binding]
public sealed class DealerServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private DealerService _dealerService;
private AppDbContext _appDbContext;
private IEnumerable<Dealer> _dealersResult;
private List<Loan> _loans;
public DealerServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
Service.Instance.ValueRetrievers.Register(new NullValueRetriever("<null>"));
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
_dealerService = new DealerService(new DealerRepository(_appDbContext));
}
[AfterScenario]
public void Dispose()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"the dealer table looks like")]
public void GivenTheDealerTableLooksLike(Table table)
{
var dealers = table.CreateSet<Dealer>().ToList();
_appDbContext.Dealers.AddRange(dealers);
_appDbContext.SaveChanges();
}
[Given(@"a list of loans with dealers that look like")]
public void GivenAListOfLoansWithDealersThatLookLike(Table table)
{
_loans = table.CreateSet<Loan>().ToList();
var dealers = table.CreateSet<Dealer>().ToList();
for (var i=0; i < _loans.Count; i++)
{
_loans.Skip(i).First().Dealer = dealers.Skip(i).First();
}
}
[When(@"I search for dealers with search string '(.*)'")]
public async void WhenISearchForDealersWithSearchString(string dealerSearchText)
{
_dealersResult = await _dealerService.Search(dealerSearchText);
}
[When(@"I update the dealers from the loans")]
public async void WhenIUpdateTheDealersFromTheLoans()
{
await _dealerService.UpdateDealersAsync(_loans);
}
[Then(@"dealers returned should be")]
public void ThenDealersReturnedShouldBe(Table table)
{
table.CompareToSet(_dealersResult);
}
[Then(@"the dealer table now looks like")]
public void ThenTheDealerTableNowLooksLike(Table table)
{
var dealers = _appDbContext.Dealers.ToList();
table.CompareToSet(dealers);
}
}
}
@@ -0,0 +1,131 @@
Feature: Export Service
Export loans to DNA that have updated reserves or chargebacks
Scenario: Export loan reserves and mark as exported
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the Core Api is successful in saving reserves
When I export the loan reserves on the date '6/1/2021 3:00 PM'
Then the Core API is called with 2 loan reserves
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 6/1/2021 3:00 PM | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 6/1/2021 3:00 PM | |
And the result of saving the reserves will be 2
Scenario: Export loan reserves - core api service returns false
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the Core Api returns false when saving reserves
When I export the loan reserves on the date '6/1/2021 3:00 PM'
Then the Core API is called with 2 loan reserves
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the result of saving the reserves will be 0
Scenario: Export loan reserves - error in service client
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the Core Api throws an error in saving reserves
When I export the loan reserves on the date '6/1/2021 3:00 PM'
Then the Core API is called with 2 loan reserves
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 5/1/2021 2:00 PM | 5/2/2021 2 pm |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the export service throws an Exception
Scenario: Export loan chargebacks and mark as exported
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | AccountType |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | | DL01 |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | DL01 |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | DL01 |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the current minor account that look like this
| MinorAccountTypeId | GeneralLedgerAccountNumber | AccountType |
| 1 | 731 | DL01 |
And the Core Api is successful in saving chargebacks
When I export the loan chargebacks on the date '5/2/2021 2:00 PM'
Then the Core API is called with 1 loan chargebacks
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate | AccountType |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | 5/2/2021 2:00 PM | DL01 |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | | DL01 |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | | DL01 |
And the result of saving the chargebacks will be 1
Scenario: Export loan chargebacks - core api returns false
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the Core Api returns false when saving chargebacks
When I export the loan chargebacks on the date '5/2/2021 2:00 PM'
Then the Core API is called with 1 loan chargebacks
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the result of saving the chargebacks will be 0
Scenario: Export loan chargebacks - error in service client
Given the current loans that look like this
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1111 | 929292929292929 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 | 121212212121212 |
| A1 | ACURA OF BROOKFIELD | 1234 | 48484848484844 |
And the Core Api throws an error in saving chargebacks
When I export the loan chargebacks on the date '5/2/2021 2:00 PM'
Then the Core API is called with 1 loan chargebacks
And the loan list will be updated to look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ExportedReserveDate | ExportedChargebackDate |
| 1 | 452 | 731 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | 50.00 | 4/29/2021 | Standard | 4/20/2021 1 pm | |
| 2 | 451 | 6 | JONES | LAURA | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 4/20/2021 1 pm | |
| 3 | 451 | 73 | JONES | LAURA | M | 1111 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | | |
And the export service throws an Exception
@@ -0,0 +1,209 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using Microsoft.EntityFrameworkCore;
using Moq;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using Xunit;
using ReserveManagementSystemLib.Configuration;
namespace ReserveManagementSystemApi.Tests.Features
{
[Binding]
public sealed class ExportServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private ExportService _exportService;
private AppDbContext _appDbContext;
private Mock<ICoreApiServiceClient> _mockCoreApiServiceClient = new Mock<ICoreApiServiceClient>();
private int _exportResult;
private Mock<RmsApiConfiguration> _mockConfiguration = new Mock<RmsApiConfiguration>();
private Exception _exportException;
public ExportServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
_mockConfiguration.Object.DefaultBatchSize = 100;
_exportService = new ExportService(new LoanRepository(_appDbContext), _mockCoreApiServiceClient.Object, new MinorAccountTypeRepository(_appDbContext), _mockConfiguration.Object);
}
[AfterScenario]
public void Dispose()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"the current loans that look like this")]
public void GivenTheCurrentLoansThatLookLikeThis(Table table)
{
var loans = table.CreateSet<Loan>();
_appDbContext.Loans.AddRange(loans);
_appDbContext.SaveChanges();
}
[Given(@"the current minor account that look like this")]
public void GivenTheCurrentMinorAccountsThatLookLikeThis(Table table)
{
var accounts = table.CreateSet<MinorAccountType>();
_appDbContext.MinorAccountTypes.AddRange(accounts);
_appDbContext.SaveChanges();
}
[Given(@"the Core Api is successful in saving reserves")]
public void GivenTheCoreApiIsSuccessfulInSavingReserves()
{
var transactionResponse = new MonetaryTransactionResponse();
transactionResponse.AccountNumber = 6;
transactionResponse.WasSuccessful = true;
var transactionResponse2 = new MonetaryTransactionResponse();
transactionResponse2.AccountNumber = 73;
transactionResponse2.WasSuccessful = true;
var transactionResponseList = new List<MonetaryTransactionResponse> { transactionResponse, transactionResponse2 };
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).ReturnsAsync(transactionResponseList);
}
[Given(@"the Core Api returns false when saving reserves")]
public void GivenTheCoreApiReturnsFalseWhenSavingReserves()
{
var transactionResponse = new MonetaryTransactionResponse();
transactionResponse.AccountNumber = 6;
transactionResponse.WasSuccessful = false;
var transactionResponse2 = new MonetaryTransactionResponse();
transactionResponse.AccountNumber = 73;
transactionResponse.WasSuccessful = false;
var transactionResponseList = new List<MonetaryTransactionResponse> { transactionResponse, transactionResponse2 };
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).ReturnsAsync(transactionResponseList);
}
[Given(@"the Core Api returns false when saving chargebacks")]
public void GivenTheCoreApiReturnsFalseWhenSavingChargebacks()
{
var transactionResponse = new MonetaryTransactionResponse();
transactionResponse.AccountNumber = 731;
transactionResponse.WasSuccessful = false;
var transactionResponseList = new List<MonetaryTransactionResponse> { transactionResponse };
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).ReturnsAsync(transactionResponseList);
}
[Given(@"the Core Api throws an error in saving reserves")]
public void GivenTheCoreApiThrowsAnErrorInSavingReserves()
{
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).Throws(new Exception());
}
[Given(@"the Core Api is successful in saving chargebacks")]
public void GivenTheCoreApiIsSuccessfulInSavingChargebacks()
{
var transactionResponse = new MonetaryTransactionResponse();
transactionResponse.AccountNumber = 731;
transactionResponse.WasSuccessful = true;
var transactionResponseList = new List<MonetaryTransactionResponse> { transactionResponse };
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).ReturnsAsync(transactionResponseList);
}
[Given(@"the Core Api throws an error in saving chargebacks")]
public void GivenTheCoreApiThrowsAnErrorInSavingChargebacks()
{
_mockCoreApiServiceClient.Setup(x => x.PostMonetaryTransactions(It.IsAny<List<GroupedMonetaryTransactionRequest>>())).Throws(new Exception());
}
[When(@"I export the loan reserves on the date '(.*)'")]
public async void WhenIExportTheLoanReservesOnTheDate(DateTime currentDate)
{
try
{
_exportResult = await _exportService.ExportReservesAsync(currentDate);
}
catch(Exception ex)
{
_exportException = ex;
}
}
[When(@"I export the loan chargebacks on the date '(.*)'")]
public async void WhenIExportTheLoanChargebacksOnTheDate(DateTime currentDate)
{
try
{
_exportResult = await _exportService.ExportChargebacksAsync(currentDate);
}
catch (Exception ex)
{
_exportException = ex;
}
}
[Then(@"the Core API is called with (.*) loan reserves")]
public void ThenTheCoreAPIIsCalledWithLoanReserves(int numberOfReserves)
{
_mockCoreApiServiceClient.Verify(x => x.PostMonetaryTransactions(It.Is<List<GroupedMonetaryTransactionRequest>>(x => x.Count == numberOfReserves)));
}
[Then(@"the Core API is called with (.*) loan chargebacks")]
public void ThenTheCoreAPIIsCalledWithLoanChargebacks(int numberOfChargebacks)
{
_mockCoreApiServiceClient.Verify(x => x.PostMonetaryTransactions(It.Is<List<GroupedMonetaryTransactionRequest>>(x => x.Count == numberOfChargebacks)));
}
[Then(@"the loan list will be updated to look like")]
public void ThenTheLoanListWillBeUpdatedToLookLike(Table table)
{
var loansResult = _appDbContext.Loans;
table.CompareToSet(loansResult);
}
[Then(@"the result of saving the reserves will be (.*)")]
public void ThenTheResultOfSavingTheReservesWillBe(int exportNumber)
{
Assert.Equal(exportNumber, _exportResult);
}
[Then(@"the result of saving the chargebacks will be (.*)")]
public void ThenTheResultOfSavingTheChargebacksWillBe(int exportNumber)
{
Assert.Equal(exportNumber, _exportResult);
}
[Then(@"the export service throws an Exception")]
public void ThenTheExportServiceThrowsAnException()
{
Assert.IsType<Exception>(_exportException);
}
[Given(@"the dealers that look like")]
public void GivenTheDealersThatLookLike(Table table)
{
var dealers = table.CreateSet<Dealer>().ToList();
_appDbContext.Dealers.AddRange(dealers);
_appDbContext.SaveChanges();
}
}
}
@@ -0,0 +1,392 @@
Feature: Import Read Service - Closed Loans from DNA
Gets closed loans from DNA by calling a CoreApiWrapper endpoint, reads loans into memory, calculates chargebacks, saves loans, saves import information
Background:
Given a loan type table consisting of
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
| BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 |
| NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 |
Scenario: AddImport for Closed Loans from DNA - all loans are saved
Given that I get these closed loans back from DNA
| LoanNumber | PaidLoanDate |
| 147 | 02/25/2021 |
| 143 | 02/12/2021 |
| 144 | 02/12/2021 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose |
| 1 | 1 | 5841 | 147 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | |
| 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | |
| 3 | 2 | 100947 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | |
When I process the closed loan import from DNA at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 100947 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 1 history/ies having OldValue "Updated" and NewValue "Closed"
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Loans from DNA - saves two loans that are in the loan table
Given that I get these closed loans back from DNA
| LoanNumber | PaidLoanDate |
| 141 | 04/25/2021 |
| 143 | 02/12/2021 |
| 144 | 02/12/2021 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus |
| 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New |
| 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
| 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
When I process the closed loan import with missing loan "141" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 12 | 3 | | | New | |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | Term |
| 3250 | 141 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 5.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 4/25/2021 | 63 |
When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 |
And the import has an end date that is greater or equal to the start date
And the loans with details now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 3250 | 141 | 720.6656 | 04/25/2021 | Closed | 60 |
Scenario: AddImport for Closed Boat Loans from DNA - saves two boat loans that are in the loan table
Given that I get these closed loans back from DNA
| LoanNumber | PaidLoanDate |
| 147 | 02/25/2021 |
| 143 | 04/12/2021 |
| 144 | 04/12/2021 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | AccountType |
| 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | DL09 |
| 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | DL10 |
| 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | DL11 |
When I process the closed loan import with missing loan "147" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 12 | 3 | | | New | |
| 2 | 10094 | 143 | 1402.7417 | 4/12/2021 | Closed | 59 |
| 3 | 10621 | 144 | 1402.7417 | 4/12/2021 | Closed | 59 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays | AccountType |
| 3250 | 147 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 2/25/2021 | 1918 | DL09 |
When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 |
And the import has an end date that is greater or equal to the start date
And the loans with details now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 3250 | 147 | 769.1038 | 02/25/2021 | Closed | 62 |
Scenario: AddImport for Closed Loans from DNA - Set Action Required = false
Given that I get these closed loans back from DNA
| LoanNumber | PaidLoanDate |
| 148 | 05/25/2021 |
| 143 | 02/12/2021 |
| 144 | 02/12/2021 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | ActionRequired |
| 1 | 1 | 12 | 3 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | false |
| 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true |
| 3 | 2 | 10621 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true |
When I process the closed loan import with missing loan "148" with date from DNA at "3/5/2021 5:00:00 AM" and select from "2/25/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 2 | 2 | true | | Closed | 1 |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | ActionRequired |
| 1 | 12 | 3 | | | New | | false |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | false |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | false |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | Term |
| 3250 | 148 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 5/25/2021 | 63 |
When I process the missing active loans with detail from DNA at "3/5/2021 5:00:00 AM" and export
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type | ImportId |
| 3/5/2021 5:00:00 AM | 1 | 1 | true | | Closed | 2 |
And the import has an end date that is greater or equal to the start date
And the loans with details now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 3250 | 147 | 0 | 05/25/2021 | Closed | 59 |
Scenario: AddImport for Closed Loans from DNA - no loans are returned
Given that I get these closed loans back from DNA
| LoanNumber | PaidLoanDate |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose |
| 1 | 1 | 5841 | 147 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | |
| 2 | 2 | 10094 | 143 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | |
| 3 | 2 | 100947 | 144 | JOHNSON | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | |
When I process the closed loan import from DNA at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 0 | 0 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And ExportChargebacksAsync is called
Scenario: Import Closed Loans from DNA with loan details, first adding them, starting at a certain closed date
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, skipping the exports
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" and skip the export
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And MarkReservesAndChargebacksAsExportedAsync is called
And ExportChargebacksAsync is not called
And ExportReservesAsync is not called
Scenario: Import Closed Loans from DNA with loan details, two new loans and one updated (changed member initial)
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, two new loans and one updated (to change contract rate)
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5.5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 524.41373886682351065543659741 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, with skip the export
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5.5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM" and skip the export
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 1 | 1 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 524.41373886682351065543659741 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 1 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And MarkReservesAndChargebacksAsExportedAsync is called
And ExportChargebacksAsync is not called
And ExportReservesAsync is not called
Scenario: Import Closed Loans from DNA with loan details, two new loans and one existing which is already closed with same paid loan date - should ignore that
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, two new loans and one existing which is already closed but had a different paid loan date
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, Set Action Required = false
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | Y | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment | ActionRequired |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | | false |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | | false |
| 3 | 2233 | 2233143 | JONES | FRED | Y | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | | false |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
And ExportReservesAsync is called
Scenario: Import Closed Loans from DNA with loan details, all dealers are added
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1121 |
And dealers that look like
| Dealer Code | Name | Address1 | Address2 | City | State | Zip | DealerOrganizationNumber | DealerAccountNumber |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And the dealers now look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 |
Scenario: Import Closed Loans from DNA with loan details, all dealers are added, but only dealer information from first loan is added
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | | | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1120 |
And dealers that look like
| Dealer Code | Name | Address1 | Address2 | City | State | Zip | DealerOrganizationNumber | DealerAccountNumber |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And the dealers now look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | | 3434 | |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 |
Scenario: Import Closed Loans from DNA with loan details, dealers are updated
Given an import from DNA that contains
| Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | LoanType | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | LoanAmount | Buy Rate | Contract Rate | Gap | Warranty | Mbp | CreditLife | CreditDisability | DealerOrganizationNumber | DealerAccountNumber | DealerName | ApplicationId | VIN | Make | Model | Year | Mileage | Color | PaidLoanDate | OriginalTermInDays |
| 3250 | 3250144 | MUELLER | STEVEN | C | S7 | NC | 2/10/2021 | 02/03/2021 | 03/05/2021 | 05/05/2026 | 27889.51 | 2.03 | 3.39 | True | False | False | False | True | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 2123444 | | | | | | | 6/30/2021 | 1918 |
| 12021 | 12021143 | HAYDEN | ALEXANDER | W | H21 | BB | 10/31/2019 | 11/6/2019 | 12/15/2019 | 2/15/2026 | 20171.51 | 3.29 | 4.90 | False | True | False | True | False | 3488 | 9888447543454544 | HEISER CHEVROLET CADILLAC OF WEST BEND | 232323455 | M233333389Y853128921 | Honda | Civic | 2020 | 1900 | Black | 7/1/2021 | 2294 |
| 2233 | 2233143 | JONES | FRED | X | S7 | NC | 2/1/2021 | 2/5/2021 | 4/1/2021 | 3/1/2024 | 20000 | 3.5 | 5 | false | true | true | false | false | 3434 | 324324324324324 | SUBURBAN MOTORS HARLEY DAVIDSON | 21235 | M34kls | Toyota | Camry | 2019 | 5 | Red | 7/3/2021 | 1120 |
And dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HD | 3434 | 324324324324324 |
When I process the closed loan import with detail from DNA at "8/1/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 8/1/2021 5:00:00 AM | 3 | 3 | true | | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Term | FlatFeePaid | ReserveAmount | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | Adjustment |
| 1 | 3250 | 3250144 | MUELLER | STEVEN | C | 63 | false | 793.9136070432343049958707190 | 0 | 6/30/2021 | Closed | 58 | |
| 2 | 12021 | 12021143 | HAYDEN | ALEXANDER | W | 75 | false | 1132.5564086943452341656244555 | 611.9778 | 7/1/2021 | Closed | 55 | |
| 3 | 2233 | 2233143 | JONES | FRED | X | 37 | false | 392.12008534200236229531665461 | 0 | 7/3/2021 | Closed | 33 | |
And the dealers now look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 3488 | 9888447543454544 |
@@ -0,0 +1,194 @@
Feature: Import Read Service - Closed Loans
Gets closed loans from a stream of csv data, reads loans into memory, calculates chargebacks, saves loans, saves import information
Scenario: AddImport for Closed Loans - all loans are saved
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidLoanDate
147,02/25/2021
143,02/12/2021
144,02/12/2021
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | RemainingMonthsAtClose |
| 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | Updated | |
| 3 | 2 | 100947 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 100947 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 1 history/ies having OldValue "Updated" and NewValue "Closed"
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Loans - trim fields and header
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidLoanDate
147, 02/25/2021
143, 02/12/2021
144, 02/12/2021
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus |
| 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
| 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Loans - Set Action Required to False
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidLoanDate
147, 02/25/2021
143, 02/12/2021
144, 02/12/2021
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | ActionRequired |
| 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | true |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | false |
| 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New | true |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose | ActionRequired |
| 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 | false |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 | false |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 | false |
And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Loans - header case insensitive
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
loannumber,PaidLoanDate
147, 02/25/2021
143, 02/12/2021
144, 02/12/2021
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus |
| 1 | 1 | 5841 | 147 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
| 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 3 | true | ClosedLoans 2021-03-01.txt | Closed |
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 5841 | 147 | 210.0375 | 2/25/2021 | Closed | 63 |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And the import has an end date that is greater or equal to the start date
And AddHistoriesAsync is called with 3 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 3 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Loans - saves two loans that are in the loan table
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidLoanDate
149,02/25/2021
143,02/12/2021
144,02/12/2021
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus |
| 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
| 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | New |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 2 | true | ClosedLoans 2021-03-01.txt | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 12 | 3 | | | New | |
| 2 | 10094 | 143 | 1498.6354 | 2/12/2021 | Closed | 61 |
| 3 | 10621 | 144 | 1498.6354 | 2/12/2021 | Closed | 61 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And AddHistoriesAsync is called with 1 history/ies having ActivityType "Error" and AuditDescription "Loan in closed import not found"
And ExportChargebacksAsync is called
Scenario: AddImport for Closed Boat Loans - saves two loans that are in the loan table
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidLoanDate,AccountType
151,02/25/2021,DL10
143,04/12/2021,DL11
144,04/12/2021,DL12
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | LoanStatus | AccountType |
| 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | New | DL10 |
| 2 | 2 | 10094 | 143 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message | New | DL11 |
| 3 | 2 | 10621 | 144 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 1/10/2027 | 21274.24 | 2.490 | 4.470 | NC | 70 | 1063.7120 | 500 | false | | | Message | New | DL12 |
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 3 | 2 | true | ClosedLoans 2021-03-01.txt | Closed |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | Member Number | Loan Number | Chargeback | Paid Loan Date | LoanStatus | RemainingMonthsAtClose |
| 1 | 12 | 3 | | | New | |
| 2 | 10094 | 143 | 1402.7417 | 4/12/2021 | Closed | 59 |
| 3 | 10621 | 144 | 1033.7483 | 4/12/2021 | Closed | 69 |
And AddHistoriesAsync is called with 2 history/ies having OldValue "New" and NewValue "Closed"
And AddHistoriesAsync is called with 2 history/ies having ActivityType "LoanStatusChange" and AuditDescription "Importing closed loan changed loan status"
And AddHistoriesAsync is called with 1 history/ies having ActivityType "Error" and AuditDescription "Loan in closed import not found"
And ExportChargebacksAsync is called
Scenario: AddImport - file has a header with one column named incorrectly
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
LoanNumber,PaidDate
147,02/25/2021
143,02/12/2021
144,02/12/2021
"""
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
And I get the record from the ImportTable
Then the import looks like
| StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | | 0 | 0 | false | ClosedLoans 2021-03-01.txt | Closed |
Scenario: AddImport - file is missing the header
Given an import file named "ClosedLoans 2021-03-01.txt" that contains
"""
147,02/25/2021
143,02/12/2021
144,02/12/2021
"""
When I read this closed loan import file at "3/5/2021 5:00:00 AM"
And I get the record from the ImportTable
Then the import looks like
| StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | | 0 | 0 | false | ClosedLoans 2021-03-01.txt | Closed |
@@ -0,0 +1,234 @@
Feature: Import Read Service - New Loans
Gets loans from a stream of csv data, reads loans into memory, calculates reserve, saves loans, saves import information
Scenario: AddImport - happy path
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462
4545,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2189
"""
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 2 | 2 | true | NewLoans 2021-03-01.txt | Active |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 3434 | 2/10/2021 | 2.03 | 3.39 | New | | 48 |
| 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 4545 | 1/23/2021 | 4.39 | 6.39 | New | | 72 |
And ExportReservesAsync is called
Scenario: AddImport with Boat Loan Account Type
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,4.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black,1245988,false,false,false,false,false,DL10
"""
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 1 | 1 | true | NewLoans 2021-03-01.txt | Active |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | ReserveAmount | ReserveMessage | Term |
| 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 4.49 | New | | 1673.3706 | Standard Reserve | 63 |
And ExportReservesAsync is called
Scenario: AddImport - file is wrong format (missing column, etc.) - note that this doesn't return the import that was saved but it is in the table
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
C9,3250,144,MUELLER, STEVEN C,02/10/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N
R23,12021,143,HAYDEN, ALEXANDER W,01/23/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N
"""
When I read this new loan import file at "3/5/2021 5:00:00 AM"
And I get the record from the ImportTable
Then the import looks like
| StartDate | EndDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | | 0 | 0 | false | NewLoans 2021-03-01.txt | Active |
And ExportReservesAsync is not called
Scenario: AddImport - file name has more than 50 characters
Given an import file named "NewLoans 2021-03-011234567890123456789012345678901234567890.txt" that contains
"""
1234,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462
5678,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2189
"""
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename |
| 3/5/2021 5:00:00 AM | 2 | 2 | true | NewLoans 2021-03-011234567890123456789012345678901 |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| ImportId | Member Number | Loan Number | Member LastName | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 2.03 | 3.39 | New | | 48 |
| 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 5678 | 1/23/2021 | 4.39 | 6.39 | New | | 72 |
And ExportReservesAsync is called
Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, saves one brand new loan
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03,5.790,579.76,NC,XX,N,1930
4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462
1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2199
7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.030 | 4.790 | New | 5/2/2021 | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.39 | New | 5/2/2021 | 72 |
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename |
| 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.03 | 5.790 | Updated | | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.39 | Updated | | 72 |
| 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated"
And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13"
And ExportReservesAsync is called
Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round down
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03124,5.790,579.76,NC,XX,N,1930
4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462
1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.39,251.61,NC,XX,N,2199
7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.031 | 4.790 | New | 5/2/2021 | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.39 | New | 5/2/2021 | 72 |
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename |
| 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.031 | 5.790 | Updated | | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.39 | Updated | | 72 |
| 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated"
And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13"
And ExportReservesAsync is called
Scenario: AddImport for Update Loans - updates three new loans that are in the loan table, one loan has ten thousandth decimal place difference in buy rate, and same with contract rate, round up
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,13,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03155,5.790,579.76,NC,XX,N,1930
4545,3250,144,MUELLER, STEVEN C,02/10/2021,02/12/2021,03/12/2021,2/12/2025,25989.97,2.03,3.39,579.76,NC,XX,N,1462
1212,12021,143,HAYDEN, ALEXANDER W,01/23/2021,02/02/2021,03/09/2021,2/9/2027,14975.57,4.39,6.2465,251.61,NC,XX,N,2199
7878,13991,145,RUTHENBERG, RICHARD J,01/22/2021,02/02/2021,03/08/2021,5/8/2027,32891.50,2.43,3.19,484.92,NC,XX,N,2287
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.031 | 5.790 | New | 5/2/2021 | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 | 2.03 | 3.39 | New | 5/2/2021 | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 | 4.39 | 6.246 | New | 5/2/2021 | 72 |
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename |
| 3/5/2021 5:00:00 AM | 4 | 4 | true | NewLoans 2021-03-01.txt |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 13 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.032 | 5.790 | Updated | | 63 |
| 2 | 1 | 3250 | 144 | MUELLER | STEVEN | C | 4545 | 2/10/2021 12:00:00 AM | 2.03 | 3.39 | Updated | | 48 |
| 3 | 1 | 12021 | 143 | HAYDEN | ALEXANDER | W | 1212 | 1/23/2021 12:00:00 AM | 4.39 | 6.247 | Updated | | 72 |
| 4 | 1 | 13991 | 145 | RUTHENBERG | RICHARD | J | 7878 | 1/22/2021 12:00:00 AM | 2.43 | 3.19 | New | | 75 |
And AddHistoriesAsync is called with 3 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated"
And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "13"
And ExportReservesAsync is called
Scenario: AddImport for Update Loans - updates one loan that is in the loan table, 1 records to audit table for updates
Given an import file named "NewLoans 2021-03-01.txt" that contains
"""
3434,12,3,SMITH, JOE,02/25/2021,02/28/2021,04/11/2021,6/11/2026,12615.87,4.03,5.790,579.76,NC,XX,N,1930
"""
And a loan table consisting of
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 12 | 3 | SMITH | JOE | | 3434 | 2/25/2021 | 4.030 | 4.790 | New | 5/2/2021 | 63 |
When I read this new loan import file at "3/5/2021 5:00:00 AM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 3/5/2021 5:00:00 AM | 1 | 1 | true | NewLoans 2021-03-01.txt | Active |
And the import has an end date that is greater or equal to the start date
And the loans now look like
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | LoanStatus | ExportedReserveDate | Term |
| 1 | 1 | 12 | 3 | SMITH | JOE | | 3434 | 2/25/2021 12:00:00 AM | 4.03 | 5.790 | Updated | | 63 |
And AddHistoriesAsync is called with 1 history/ies having ActivityType "NewLoanUpdate" and AuditDescription "Loan found in table, record updated"
And AddHistoriesAsync is called with 1 history/ies having LoanNumber "3" and MemberNumber "12"
And ExportReservesAsync is called
Scenario: AddImport - Vehicle Info - One record populated with VIN, one record optional, Not checking for invalid dealer code any more, Two Invalid Missing VIN action required True
Given an import file named "NewLoans 2021-05-19.txt" that contains
"""
3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black
4545,7488740,144,STERLING, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918
4545,8488740,144,STERLING, KIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,,Honda,Civic,2020,1900,Black
"""
And dealers that look like
| Dealer Code | Name | DealerOrganizationNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3333 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 4545 |
When I read this new loan import file at "5/19/2021 1:00:00 PM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 5/19/2021 1:00:00 PM | 3 | 3 | true | NewLoans 2021-05-19.txt | Active |
And the loans now look like
| ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | Action Required | ExportedReserveDate | Term |
| 1 | 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 2.49 | New | False | | 63 |
| 1 | 2 | 7488740 | 144 | STERLING | JIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | True | | 63 |
| 1 | 3 | 8488740 | 144 | STERLING | KIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | True | | 63 |
And ExportReservesAsync is called
Scenario: AddImport - Vehicle Info - One record populated with VIN, one record optional
Given an import file named "NewLoans 2021-05-19.txt" that contains
"""
3434,71229741,143,WILLIAMS,SAM R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918
4545,7488740,144,STERLING, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y85312,Honda,Civic,2020,1900,Black
"""
When I read this new loan import file at "5/19/2021 1:00:00 PM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 5/19/2021 1:00:00 PM | 2 | 2 | true | NewLoans 2021-05-19.txt | Active |
And the loans now look like
| ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term |
| 1 | 1 | 71229741 | 143 | WILLIAMS | SAM | R | 3434 | 1/06/2021 | 2.49 | 2.49 | New | | 63 |
| 1 | 2 | 7488740 | 144 | STERLING | JIM | C | 4545 | 1/06/2021 | 2.49 | 2.49 | New | | 63 |
And the vehicle for LoanId 2 looks like this
| VIN Number | LoanId | Make | Model | Year | Mileage | Color |
| M233333389Y85312 | 2 | Honda | Civic | 2020 | 1900 | Black |
And the vehicle for LoanId 1 looks like this
| VIN Number | LoanId | Make | Model | Year | Mileage | Color |
| | 1 | | | | 0 | |
And ExportReservesAsync is called
Scenario: AddImport - Vehicle Info - Both records populated with VIN, can handle VIN longer than 17 characters
Given an import file named "NewLoans 2021-05-19-2.txt" that contains
"""
3434,7488740,144,SWEENEY, JIM C,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M733P33389Y85216,Chevrolet,Cruze,2013,10K,White
4545,71229741,143,BARNES,BILL R,01/06/2021,02/03/2021,03/05/2021,05/05/2026,27889.51,2.49,2.49,472.71,NC,XX,N,1918,M233333389Y853128921,Buick,LaCrosse,2017,56000,Burgundy
"""
When I read this new loan import file at "5/19/2021 1:00:00 PM"
Then the import looks like
| StartDate | RecordCount | RecordsSaved | Success | Filename | Import Type |
| 5/19/2021 1:00:00 PM | 2 | 2 | true | NewLoans 2021-05-19-2.txt | Active |
And the loans now look like
| ImportId | LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | Buy Rate | Contract Rate | Loan Status | ExportedReserveDate | Term |
| 1 | 1 | 7488740 | 144 | SWEENEY | JIM | C | 3434 | 1/06/2021 | 2.49 | 2.49 | New | | 63 |
| 1 | 2 | 71229741 | 143 | BARNES | BILL | R | 4545 | 1/06/2021 | 2.49 | 2.49 | New | | 63 |
And the vehicle for LoanId 1 looks like this
| VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color |
| 1 | M733P33389Y85216 | 1 | Chevrolet | Cruze | 2013 | 0 | White |
And the vehicle for LoanId 2 looks like this
| VehicleId | VIN Number | LoanId | Make | Model | Year | Mileage | Color |
| 2 | M233333389Y853128921 | 2 | Buick | LaCrosse | 2017 | 56000 | Burgundy |
And ExportReservesAsync is called
@@ -0,0 +1,434 @@
using Microsoft.EntityFrameworkCore;
using Moq;
using System.Globalization;
using System.Text;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using TechTalk.SpecFlow.Assist.ValueRetrievers;
using Xunit;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using LCU.Library.Internal.ApiSecurity.Enums;
using LCU.Library.Internal.ApiSecurity.Models;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
namespace ReserveManagementSystemApi.Tests.Features
{
[Binding]
public sealed class ImportReadServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private ApiSecurityConfiguration _apiSecurityConfiguration;
private ImportReadService _importReadService;
private LoanRepository _loanRepository;
private LoanTypeRepository _loanTypeRepository;
private DealerRepository _dealerRepository;
private DealerService _dealerService;
private Mock<IAuditHistoryRepository> _mockAuditHistRepo;
private Mock<IExportService> _mockExportService;
private LoanService _loanService;
private ImportRepository _importRepository;
private ImportService _importService;
private AppDbContext _appDbContext;
private Import _importResult;
private Exception _exception;
private string _importFileName;
private StreamReader _importStreamReader;
private List<Loan> _loansResult;
private List<Dealer> _dealersResult;
private List<IndirectLoanDetail> _loanDetailsFromDNA;
private List<ClosedIndirectLoan> _closedLoansFromDNA;
private Mock<CoreApiConfiguration> _mockCoreApiConfiguration;
private Mock<IGetRequest> _mockgetRequest;
private Mock<IPostRequest> _mockpostRequest;
private Mock<IPutRequest> _mockputRequest;
public ImportReadServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
Service.Instance.ValueRetrievers.Register(new NullValueRetriever("<null>"));
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_apiSecurityConfiguration = new ApiSecurityConfiguration(ApiKeyAuthenticationFailureBehavior.Warn);
_mockCoreApiConfiguration = new Mock<CoreApiConfiguration>();
_mockCoreApiConfiguration.Object.ApiTimeOutInSeconds = "420";
_mockpostRequest = new Mock<IPostRequest>();
_mockputRequest = new Mock<IPutRequest>();
_mockgetRequest = new Mock<IGetRequest>();
_appDbContext = new AppDbContext(options);
_loanRepository = new LoanRepository(_appDbContext);
_loanTypeRepository = new LoanTypeRepository(_appDbContext);
_dealerRepository = new DealerRepository(_appDbContext);
_importRepository = new ImportRepository(_appDbContext);
_mockAuditHistRepo = new Mock<IAuditHistoryRepository>();
_mockExportService = new Mock<IExportService>();
_dealerService = new DealerService(_dealerRepository);
_loanService = new LoanService(_loanRepository, _loanTypeRepository, new AuditHistoryService(_mockAuditHistRepo.Object),
new Mock<ICoreApiServiceClient>().Object);
_importService = new ImportService(_importRepository);
_importReadService = new ImportReadService(
_loanService, _importService,
_dealerService,
_mockExportService.Object,
new CoreApiServiceClient(_apiSecurityConfiguration, _mockCoreApiConfiguration.Object, _mockgetRequest.Object, _mockpostRequest.Object, _mockputRequest.Object),
new Mock<RmsApiConfiguration>().Object
);
}
[AfterScenario]
public void DisposeAppDbContext()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"an import file named ""(.*)"" that contains")]
public void GivenAnImportFileNamedThatContains(string fileName, string fileContent)
{
_importFileName = fileName;
var byteArray = Encoding.UTF8.GetBytes(fileContent);
var stream = new MemoryStream(byteArray);
_importStreamReader = new StreamReader(stream);
}
[Given(@"that I get these closed loans back from DNA")]
public void GivenThatIGetTheseClosedLoansBackFromDNA(Table table)
{
_closedLoansFromDNA = table.CreateSet<ClosedIndirectLoan>().ToList();
}
[Given(@"an import from DNA that contains")]
public void GivenAnImportFromDNAThatContains(Table table)
{
_loanDetailsFromDNA = table.CreateSet<IndirectLoanDetail>().ToList();
}
[Given(@"a loan table consisting of")]
public void GivenALoanTableConsistingOf(Table table)
{
var loans = table.CreateSet<Loan>().ToList();
_appDbContext.Loans.AddRange(loans);
_appDbContext.SaveChanges();
}
[Given(@"a loan type table consisting of")]
public void GivenALoanTypeTableConsistingOf(Table table)
{
var types = table.CreateSet<LoanType>().ToList();
_appDbContext.LoanTypes.AddRange(types);
_appDbContext.SaveChanges();
}
[Given(@"dealers that look like")]
public void GivenDealersThatLookLike(Table table)
{
var dealers = table.CreateSet<Dealer>().ToList();
_appDbContext.Dealers.AddRange(dealers);
_appDbContext.SaveChanges();
}
[Given(@"the current vehicle for loanId (.*) is")]
public void GivenTheCurrentVehicleForLoanIdIs(int loanId, Table table)
{
var vehicle = table.CreateInstance<Vehicle>();
vehicle.LoanId = loanId;
_appDbContext.Vehicles.Add(vehicle);
_appDbContext.SaveChanges();
}
[When(@"I read this new loan import file at ""(.*)""")]
public async void WhenIReadThisNewLoanImportFileAt(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ReadNewLoanImportFile(_importStreamReader, _importFileName, currentTime);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process this DNA new/updated loan import at ""(.*)""")]
public async void WhenIReadThisNewDNALoanImportFileAt(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessActiveLoanImport(_loanDetailsFromDNA, currentTime);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process this DNA new/updated loan import at ""(.*)"" and skip the export")]
public async void WhenIProcessThisDNANewUpdatedLoanImportAtAndSkipTheExport(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessActiveLoanImport(_loanDetailsFromDNA, currentTime, true);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process the closed loan import from DNA at ""(.*)""")]
public async void WhenIReadTheClosedLoanImportFromDNAAt(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessClosedLoanImport(_closedLoansFromDNA, currentTime, string.Empty);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process the closed loan import with missing loan ""(.*)"" with date from DNA at ""(.*)"" and select from ""(.*)""")]
public async void WhenIProcessTheClosedLoanImportWithMissingLoanWithDateFromDNAAt(string missingLoanNumber, DateTime currentTime, DateTime newerThan)
{
try
{
var stringDate = newerThan.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
_importResult = await _importReadService.ProcessClosedLoanImport(_closedLoansFromDNA.Where(l => l.LoanNumber != missingLoanNumber).ToList(),
currentTime, stringDate);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process the missing active loans with detail from DNA at ""(.*)"" and export")]
public async void WhenIProcessTheMissingActiveLoansWithDetailFromDNAAtAndExport(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, false);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process the closed loan import with detail from DNA at ""(.*)""")]
public async void WhenIReadTheInitialClosedLoanImportFromDNAAt(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, false);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I process the closed loan import with detail from DNA at ""(.*)"" and skip the export")]
public async void WhenIReadTheInitialClosedLoanImportFromDNAAtAndSkipTheExport(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ProcessClosedLoansWithUpdates(_loanDetailsFromDNA, currentTime, true);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I read this closed loan import file at ""(.*)""")]
public async void WhenIReadThisClosedLoanImportFileAt(DateTime currentTime)
{
try
{
_importResult = await _importReadService.ReadClosedLoanImportFile(_importStreamReader, _importFileName, currentTime);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the record from the ImportTable")]
public async void WhenIGetTheRecordFromTheImportTable()
{
_importResult = (await _importRepository.GetAll().ToListAsync()).First();
}
[Then(@"the import has start date ""(.*)""")]
public void ThenTheImportHasStartDate(DateTime startDate)
{
Assert.Equal(startDate, _importResult.StartDate);
}
[Then(@"the import has an end date that is greater or equal to the start date")]
public void ThenTheImportHasAnEndDateThatIsGreaterOrEqualToTheStartDate()
{
Assert.True(_importResult.EndDate >= _importResult.StartDate);
}
[Then(@"the import has record count (.*)")]
public void ThenTheImportHasRecordCount(int recordCount)
{
Assert.Equal(recordCount, _importResult.RecordCount);
}
[Then(@"the import has success ""(.*)""")]
public void ThenTheImportHasSuccess(bool success)
{
Assert.Equal(success, _importResult.Success);
}
[Then(@"the import has file name ""(.*)""")]
public void ThenTheImportHasFileName(string fileName)
{
Assert.Equal(fileName, _importResult.FileName);
}
[Then(@"an exception is thrown")]
public void ThenAnExceptionIsThrown()
{
Assert.NotNull(_exception);
}
[Then(@"the import looks like")]
public void ThenTheImportLooksLike(Table table)
{
table.CompareToInstance(_importResult);
}
[Then(@"there is a record added to the Audit History table")]
public void ThenThereIsARecordAddedToTheAuditHistoryTable()
{
_mockAuditHistRepo.Verify(x => x.AddHistoriesAsync(It.IsAny<List<AuditHistory>>()));
}
[Then(@"no audit history record is added")]
public void ThenNoAuditHistoryRecordIsAdded()
{
_mockAuditHistRepo.Verify(x => x.AddHistoriesAsync(It.IsAny<List<AuditHistory>>()), Times.Never);
}
[Then(@"AddHistoriesAsync is called with (.*) history/ies having OldValue ""(.*)"" and NewValue ""(.*)""")]
public void ThenAddHistoriesAsyncIsCalledWithHistoryIesHavingOldValueAndNewValue(int number, string oldValue, string newValue)
{
_mockAuditHistRepo.Verify(x =>
x.AddHistoriesAsync(It.Is<List<AuditHistory>>(a => a.Count(x => x.NewValue == newValue && x.OldValue == oldValue) == number)));
}
[Then(@"AddHistoriesAsync is called with (.*) history/ies having ActivityType ""(.*)"" and AuditDescription ""(.*)""")]
public void ThenAddHistoriesAsyncIsCalledWithHistoryHavingActivityTypeAndAuditDescription(int number, string type, string description)
{
_mockAuditHistRepo.Verify(x =>
x.AddHistoriesAsync(It.Is<List<AuditHistory>>(a => a.Count(x => x.ActivityType == type && x.AuditDescription == description) == number)));
}
[Then(@"AddHistoriesAsync is called with (.*) history/ies having LoanNumber ""(.*)"" and MemberNumber ""(.*)""")]
public void ThenAddHistoriesAsyncIsCalledWithHistoryHavingLoanNumberAndMemberNumber(int number, string loanNumber, string memberNumber)
{
_mockAuditHistRepo.Verify(x =>
x.AddHistoriesAsync(It.Is<List<AuditHistory>>(a => a.Count(x => x.MemberNumber == memberNumber && x.LoanNumber == loanNumber) == number)));
}
[Then(@"ExportChargebacksAsync is called")]
public void ThenExportChargebacksAsyncIsCalled()
{
_mockExportService.Verify(x => x.ExportChargebacksAsync(It.IsAny<DateTime>()));
}
[Then(@"ExportChargebacksAsync is not called")]
public void ThenExportChargebacksAsyncIsNotCalled()
{
_mockExportService.Verify(x => x.ExportChargebacksAsync(It.IsAny<DateTime>()), Times.Never);
}
[Then(@"MarkReservesAndChargebacksAsExportedAsync is called")]
public void ThenMarkReservesAndChargebacksAsExportedAsyncIsCalled()
{
_mockExportService.Verify(x => x.MarkReservesAndChargebacksAsExportedAsync(It.IsAny<List<Loan>>(), It.IsAny<DateTime>()));
}
[Then(@"all loans have non-null ExportedChargebackDate")]
public void ThenAllLoansHaveNon_NullExportedChargebackDate()
{
Assert.DoesNotContain(_loansResult, l => l.ExportedChargebackDate == null);
}
[Then(@"ExportReservesAsync is called")]
public void ThenExportReservesAsyncIsCalled()
{
_mockExportService.Verify(x => x.ExportReservesAsync(It.IsAny<DateTime>()));
}
[Then(@"ExportReservesAsync is not called")]
public void ThenExportReservesAsyncIsNotCalled()
{
_mockExportService.Verify(x => x.ExportReservesAsync(It.IsAny<DateTime>()), Times.Never);
}
[Then(@"MarkReservesAsExportedAsync is called")]
public void ThenMarkReservesAsExportedAsyncIsCalled()
{
_mockExportService.Verify(x => x.MarkReservesAsExportedAsync(It.IsAny<List<Loan>>(), It.IsAny<DateTime>()));
}
[Then(@"all loans have non-null ExportedReserveDate")]
public void ThenAllLoansHaveNon_NullExportedReserveDate()
{
Assert.DoesNotContain(_loansResult, l => l.ExportedReserveDate == null);
}
[Then(@"the loans now look like")]
public async void ThenTheLoansNowLookLike(Table table)
{
_loansResult = await _loanRepository.GetAll().AsNoTracking().ToListAsync();
table.CompareToSet(_loansResult);
}
[Then(@"the dealers now look like")]
public async void ThenTheDealersNowLookLike(Table table)
{
_dealersResult = await _dealerRepository.GetAll().ToListAsync();
table.CompareToSet(_dealersResult);
}
[Then(@"the vehicle for LoanId (.*) looks like this")]
public void ThenTheVehicleForLoanIdLooksLikeThis(int loanId, Table table)
{
var result = _loansResult.FirstOrDefault(v => v.LoanId == loanId)?.Vehicle;
table.CompareToInstance<Vehicle>(result);
}
[Then(@"the vehicle for LoanId (.*) is null")]
public void ThenTheVehicleForLoanIdIsNull(int loanId)
{
Assert.Null(_loansResult.FirstOrDefault(v => v.LoanId == loanId).Vehicle);
}
[Then(@"the loans with details now look like")]
public void ThenTheLoansWithDetailsNowLookLike(Table table)
{
var loans = table.CreateInstance<Loan>();
_loansResult.Add(loans);
}
}
}
@@ -0,0 +1,92 @@
Feature: ImportService
Gets Imports from database
Scenario: GetAllAsync Success
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |
When I get the list of all imports with the import service
Then the result from the import service should have 3 imports
And the first import from the import service has ImportId 3
Scenario: GetAllAsync Exception
Given the Import Repository throws an exception in ListAll
When I get the list of all imports with the import service
Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: GetRecentAsync Success
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt | Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt | Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt | Active |
When I get the list of the top 2 imports with the import service
Then the result from the import service should have 2 imports
And the first import from the import service has ImportId 3
Scenario: GetRecentAsync Success - more imports asked for then are in the table
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active |
When I get the list of the top 5 imports with the import service
Then the result from the import service should have 3 imports
And the first import from the import service has ImportId 3
Scenario: GetRecentAsync Exception
Given the Import Repository throws an exception in ListAll
When I get the list of the top 5 imports with the import service
Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: GetByIdAsync Success
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active |
When I get the import with the importId 2 from the import service
Then the import from the import service has ImportId 2
Scenario: GetByIdAsync Exception - Not Found
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName |ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | New Loans 1-2-2021.txt |Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | New Loans 1-3-2021.txt |Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt |Active |
When I get the import with the importId 4 from the import service
Then the Import Service throws an exception with the message "Import with id 4 not found."
Scenario: GetByIdAsync Exception - Import Repository exception
Given the Import Repository throws an exception in FindByIdAsync
When I get the import with the importId 3 from the import service
Then the Import Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: GetLastLoanImport - active when there are only active
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | <null> | Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | <null> | Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | <null> | Active |
When I get the last loan import of type "Active"
Then the import from the import service has ImportId 3
Scenario: GetLastLoanImport - ignore imports that have a non-null filename
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | <null> | Active |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | <null> | Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | New Loans 1-4-2021.txt | Active |
When I get the last loan import of type "Active"
Then the import from the import service has ImportId 1
Scenario: GetLastLoanImport - closed when there are both types
Given the Import table has the following imports
| ImportId | Start Date | End Date | RecordCount | Success | FileName | ImportType |
| 1 | 1/2/2021 5:00:00 AM | 1/2/2021 5:10:00 AM | 25 | true | <null> | Closed |
| 2 | 1/3/2021 5:00:00 AM | 1/3/2021 5:15:00 AM | 50 | false | <null> | Active |
| 3 | 1/4/2021 5:00:00 AM | 1/4/2021 5:05:00 AM | 75 | true | <null> | Active |
When I get the last loan import of type "Closed"
Then the import from the import service has ImportId 1
@@ -0,0 +1,149 @@
using Microsoft.EntityFrameworkCore;
using Moq;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using TechTalk.SpecFlow.Assist.ValueRetrievers;
using Xunit;
using ReserveManagementSystemLib.Enums;
namespace ReserveManagementSystemApi.Tests.ServiceTests
{
[Binding]
public sealed class ImportServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private ImportService _importService;
private AppDbContext _appDbContext;
private IEnumerable<Import> _importsResult;
private Import _importResult;
private Exception _exception;
public ImportServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
Service.Instance.ValueRetrievers.Register(new NullValueRetriever("<null>"));
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
_importService = new ImportService(new ImportRepository(_appDbContext));
}
[AfterScenario]
public void DisposeAppDbContext()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"the Import table has the following imports")]
public void GivenTheImportTableHasTheFollowingImports(Table table)
{
var imports = table.CreateSet<Import>();
_appDbContext.Imports.AddRange(imports);
_appDbContext.SaveChanges();
}
[Given(@"the Import Repository throws an exception in ListAll")]
public void GivenTheImportRepositoryThrowsAnExceptionInListAll()
{
var mockImportRepo = new Mock<IImportRepository>();
mockImportRepo.Setup(a => a.GetAll()).Throws(new Exception());
_importService = new ImportService(mockImportRepo.Object);
}
[Given(@"the Import Repository throws an exception in FindByIdAsync")]
public void GivenTheImportRepositoryThrowsAnExceptionInFindByIdAsync()
{
var mockImportRepo = new Mock<IImportRepository>();
mockImportRepo.Setup(a => a.FindByIdAsync(It.IsAny<int>())).Throws(new Exception());
_importService = new ImportService(mockImportRepo.Object);
}
[When(@"I get the list of all imports with the import service")]
public async void WhenIGetTheListOfAllImportsWithTheImportService()
{
try
{
_importsResult = await _importService.GetAllAsync();
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of the top (.*) imports with the import service")]
public async void WhenIGetTheListOfAllImportsWithTheImportService(int numberOfImports)
{
try
{
_importsResult = await _importService.GetRecentAsync(numberOfImports);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the import with the importId (.*) from the import service")]
public async void WhenIGetTheImportWithTheImportIdFromTheImportService(int importId)
{
try
{
_importResult = await _importService.GetByIdAsync(importId);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the last loan import of type ""(.*)""")]
public async void WhenIGetTheLastLoanImportOfType(string importType)
{
var type = (ImportType)Enum.Parse(typeof(ImportType), importType, true);
_importResult = await _importService.GetLastLoanImportByType(type);
}
[Then(@"the result from the import service should have (.*) imports")]
public void ThenTheResultFromTheImportServiceShouldHaveImports(int importCount)
{
Assert.Equal(importCount, _importsResult.Count());
}
[Then(@"the first import from the import service has ImportId (.*)")]
public void ThenTheFirstImportFromTheImportServiceHasImportId(int importId)
{
Assert.Equal(importId, _importsResult.First().ImportId);
}
[Then(@"the Import Service throws an exception with the message ""(.*)""")]
public void ThenTheImportServiceThrowsAnExceptionWithTheMessage(string message)
{
Assert.NotNull(_exception);
Assert.Equal(message, _exception.Message);
}
[Then(@"the import from the import service has ImportId (.*)")]
public void ThenTheImportFromTheImportServiceHasImportId(int importId)
{
Assert.Equal(importId, _importResult.ImportId);
}
}
}
@@ -0,0 +1,104 @@
using Microsoft.EntityFrameworkCore;
using Moq;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using Xunit;
namespace ReserveManagementSystemApi.Tests
{
[Binding]
public sealed class LoanCalculationServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private Loan _loan;
private LoanReserve _reserve;
private LoanChargeback _chargeback;
private List<LoanType> _loanTypes;
private AppDbContext _appDbContext;
public LoanCalculationServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
}
[AfterScenario]
public void DisposeAppDbContext()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"a loan that looks like")]
public void GivenALoanThatLooksLike(Table table)
{
_loan = table.CreateInstance<Loan>();
}
[Given(@"a loan type table that looks like")]
public void GivenALoanTypeTableThatLooksLike(Table table)
{
_loanTypes = table.CreateSet<LoanType>().ToList();
_appDbContext.LoanTypes.AddRange(_loanTypes);
_appDbContext.SaveChanges();
}
[When(@"the reserve is calculated")]
public async void WhenTheReserveIsCalculated()
{
_loan.LoanType = await new LoanTypeRepository(_appDbContext).GetLoanTypeAsync(_loan.LoanTypeId);
var loanCalculationService = new LoanCalculationService();
_reserve = loanCalculationService.CalculateSingleReserve(_loan);
}
[When(@"the chargeback is calculated")]
public async void WhenTheChargebackIsCalculated()
{
_loan.LoanType = await new LoanTypeRepository(_appDbContext).GetLoanTypeAsync(_loan.LoanTypeId);
var loanCalculationService = new LoanCalculationService();
_chargeback = loanCalculationService.CalculateSingleChargeback(_loan);
}
[Then(@"FlatFeePaid should be ""(.*)""")]
public void ThenFlatFeePaidShouldBe(bool flatFeePaid)
{
Assert.Equal(flatFeePaid, _reserve.FlatFeePaid);
}
[Then(@"the reserve amount should be (.*)")]
public void ThenTheReserveAmountShouldBe(decimal reserve)
{
Assert.Equal(reserve, _reserve.ReserveAmount);
}
[Then(@"the reserve message should be (.*)")]
public void ThenTheReserveMessageShouldBe(string message)
{
Assert.Equal(message, _reserve.ReserveTypeMessage);
}
[Then(@"the chargeback returned is")]
public void ThenTheChargebackReturnedIs(Table table)
{
table.CompareToInstance<LoanChargeback>(_chargeback);
}
}
}
@@ -0,0 +1,475 @@
Feature: Loan Service - Filtering, Pagination
Get filtered list of loans, get paginated list of loans
Scenario: GetFilteredAsync Success - ImportId - sorting will be by Dealer Code ASC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 2 | 678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 3 | 2 | 22 | 6 | 901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | ImportId | DealerCodeText | ActionRequired | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | 2 | | | | | | | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "3,2"
Scenario: GetFilteredAsync Success - MemberNumber - sorting will be by Action Required DESC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 | 678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 12 | 4 | 901 | Smith | Joe | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| 12 | | | | | | | | | ActionRequired | DESC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "3,1"
Scenario: GetFilteredAsync Success - MemberName, searches both first and last name - sorting will be by MemberLastName DESC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 | 678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 12 | 4 | 901 | SMITH | JOE | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | Jo | | | | | | | | MemberLastName | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,3,1"
Scenario: GetFilteredAsync Success - MemberName, returns one - sorting will be by MemberLastName DESC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 |345 | SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 12 | 4 |901 | SMITH | JOE | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | Jon | | | | | | | | MemberLastName | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2"
Scenario: GetFilteredAsync Success - LoanNumber - sorting will be by Action Required DESC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 |345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 |678 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 56 | 3 |901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | 3 | | | | | | | ActionRequired | DESC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "3,1"
Scenario: GetFilteredAsync Success - Application Id - sorting will be by Action Required DESC importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 |345 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 |378 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 56 | 3 |901 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | 3 | | | | | | ActionRequired | DESC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,1"
Scenario: GetFilteredAsync Success - DealerCode - sorting will be by Action Required ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 0 |
| 2 | 2 | 45 | 6 | 678 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 | 901 | Johnson | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | H21 | | ActionRequired | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,3"
Scenario: GetFilteredAsync Success - ActionRequired - sorting will be by Dealer Code ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 |345 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 |901 | JOHNSON | LAURA | M | S7 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | 1 | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "1,3"
Scenario: GetFilteredAsync Success - Loan Date - start and end - sorting will be by Dealer Code ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 |SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 678 |JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 | 901 |JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | |2/23/2021 | 2/24/2021 | | | | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,3"
Scenario: GetFilteredAsync Success - Loan Date - only start date - sorting will be by Dealer Code ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number |Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 |345 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 |678 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 |901 | JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText |LoanNumberText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | 2/24/2021 | | | | | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,1"
Scenario: GetFilteredAsync Success - Loan Date - only end date - sorting will be by Dealer Code ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | SMITH | JOE | | H21 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 | JOHNSON | LAURA | M | S7 | 2/23/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | LoanDateStart | LoanDateEnd | ImportId | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | 2/24/2021 | | | | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "2,3"
Scenario: GetFilteredAsync Success - all filters used - sorting will be by DealerCode ASC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 | SMITH | JOE | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 678 | JONES | LAURA | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 3 | 2 | 22 | 2 | 901 | JOHNSON | LAURA | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| 22 | Joh | 2 | 9 | 2 | 2/24/2021 | 2/24/2021 | H21 | 1 | DealerCode | ASC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the loan ids from the loan service look like "3"
Scenario: GetFilteredAsync Success - returning nothing - sorting will be by Dealer Code DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 345 |SMITH | JOE | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 678 |JONES | LAURA | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 901 |JOHNSON | LAURA | M | H22 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | MaxRecordsPerPage | MaxPagesPerRange |
| | | | |1 | | | H21 | 0 | DealerCode | DESC | 15 | 7 |
When I get the list of the filtered loans with the loan service
Then the result from the loan service should have 0 loans
Scenario: GetPagedListAsync Success - returning page count with one index and one record validation - no filtering sorting will be by MemberFirstName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | | MemberFirstName | DESC | 2 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 2 pages
And the first result from the loan service should have loanid 13
Scenario: GetPagedListAsync Success - returning one index record count and one record validation - no filtering sorting will be by MemberLastName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | | MemberLastName | DESC | 1 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 15 records
And the first result from the loan service should have loanid 1
Scenario: GetPagedListAsync Fail - not returning pages no records - no sorting or filtering
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
Then the result from the loan service should have 0 total items
Scenario: GetPagedListAsync HasPrevious Page Success HasNext Page Fail - returning one index record count checking previous next - no sorting or filtering
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | | | | 2 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 3 records
And the result from the loan service has previous page should be true
And the result from the loan service has next page should be false
Scenario: GetPagedListAsync HasPrevious Page Fail HasNext Page Success - returning one index record count checking previous next - no sorting or filtering
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id |Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 |Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 |Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 |Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 |Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 |Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 |Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 |Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 |Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 |McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 |Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 |Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 |Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 |Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 |Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 |Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 |Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 |Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 |McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | | | | 1 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 15 records
And the result from the loan service has previous page should be false
And the result from the loan service has next page should be true
Scenario: GetPagedListAsync Success - returning page count with one index and one record validation - Filter by DealerCodeText sorting will be by MemberFirstName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
And a loan filter that looks like
| MemberNumberText | MemberFirstNameText | MemberLastNameText | ApplicationIdText | LoanNumberText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | H21 | | MemberFirstName | DESC | 1 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 1 pages
And the first result from the loan service should have loanid 8
Scenario: GetPagedListAsync Success - returning one index record count and one record validation - Filtering by ActionRequired sorting will be by MemberLastName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | | 1 | MemberLastName | DESC | 1 | 15 | 7 |
When I get the list of paged loans
Then the result from the loan service should have 8 records
And the first result from the loan service should have loanid 1
Scenario: GetPagedListAsync Success Page Range Values Check Last Page Index First and Last Page - returning one index record count and one record validation - Filtering by ImportId sorting will be by MemberLastName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Zeman | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 1 | 45 | 6 | 2 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 1 | 22 | 2 | 3 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 1 | 99 | 8 | 5 | Luke | Dwight | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 1 | 78 | 9 | 6 | Patterson | Sandy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 1 | 11 | 1 | 8 | Hayes | Willie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 1 | 11 | 2 | 9 | McGuire | Jerry | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 1 | 45 | 6 | 11 | Jones | Loren | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 1 | 22 | 2 | 12 | Johnson | Lizzie | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 1 | 99 | 8 | 14 | Luke | Delilah | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 1 | 78 | 9 | 15 | Patterson | Steve | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 1 | 11 | 1 | 17 | Hayes | Wendy | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 1 | 11 | 2 | 18 | McGuire | Sara | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 19 | 2 | 11 | 1 | 19 | Sorenson | Jill | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 20 | 2 | 11 | 2 | 20 | Berry | Bill | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | 1 | | | | | MemberLastName | DESC | 9 | 2 | 3 |
When I get the list of paged loans
Then the result from the loan service should have 2 records
And the first result from the loan service should have loanid 7
And the result from the loan service has previous page should be true
And the result from the loan service has next page should be false
And the result from the loan service first page should be 7
And the result from the loan service last page should be 9
Scenario: GetPagedListAsync Success - Page Range Values Check First and Last Page - returning one index record count and one record validation - Filtering by Dealer Code sorting will be by MemberLastName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S21 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | S21 | | MemberLastName | DESC | 5 | 2 | 3 |
When I get the list of paged loans
Then the result from the loan service should have 2 records
And the first result from the loan service should have loanid 11
And the result from the loan service has previous page should be true
And the result from the loan service has next page should be true
And the result from the loan service first page should be 4
And the result from the loan service last page should be 6
Scenario: GetPagedListAsync Success - Page Range Values Check Below Page Range Max - returning one index record count and one record validation - Filtering by Dealer Code sorting will be by MemberLastName DESC then importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Action Required |
| 1 | 1 | 12 | 3 | 1 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 2 | 2 | 45 | 6 | 2 | Jones | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 3 | 2 | 22 | 2 | 3 | Johnson | Laura | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 4 | 1 | 55 | 5 | 4 | Gallagher | Frank | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 5 | 2 | 99 | 8 | 5 | Luke | Dwight | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 6 | 2 | 78 | 9 | 6 | Patterson | Sandy | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 7 | 1 | 34 | 3 | 7 | Best | Ronald | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 8 | 2 | 11 | 1 | 8 | Hayes | Willie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 9 | 2 | 11 | 2 | 9 | McGuire | Jerry | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 10 | 1 | 12 | 3 | 10 | Smith | Joey | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 11 | 2 | 45 | 6 | 11 | Jones | Loren | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 1 |
| 12 | 2 | 22 | 2 | 12 | Johnson | Lizzie | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 13 | 1 | 55 | 5 | 13 | Gallagher | Farrah | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 14 | 2 | 99 | 8 | 14 | Luke | Delilah | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 15 | 2 | 78 | 9 | 15 | Patterson | Steve | M | 1234 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
| 16 | 1 | 34 | 3 | 16 | Best | Rainier | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard | 1 |
| 17 | 2 | 11 | 1 | 17 | Hayes | Wendy | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | 0 |
| 18 | 2 | 11 | 2 | 18 | McGuire | Sara | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message | |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S21 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
And a loan filter that looks like
| MemberNumberText | MemberNameText | LoanNumberText | ApplicationIdText | ImportId | LoanDateStart | LoanDateEnd | DealerCodeText | ActionRequired | SortField | SortDirection | PageIndex | MaxRecordsPerPage | MaxPagesPerRange |
| | | | | | | | S21 | 1 | MemberLastName | DESC | 2 | 4 | 4 |
When I get the list of paged loans
Then the result from the loan service should have 4 records
And the first result from the loan service should have loanid 4
And the result from the loan service has previous page should be true
And the result from the loan service has next page should be false
And the result from the loan service first page should be 1
And the result from the loan service last page should be 2
@@ -0,0 +1,183 @@
Feature: Loan Service Get all, Get By Id
Gets all loans, get loan by loanId
Scenario: GetListAsync Success and sorting will be by importid descending and then by member number ascending
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 2 | 22 | 2 | Johnson | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of all loans with the loan service
Then the result from the loan service should have 3 loans
And the first loan from the loan service has LoanId 3
Scenario: GetListAsync Exception
Given the Loan Repository throws an exception in ListAll
When I get the list of all loans with the loan service with the mock loan repo
Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: GetListAsync - get dealer info
Given the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 1 | 12 | 3 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 2 | 45 | 6 | Jones | Laura | M | 1111 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 2 | 22 | 2 | Johnson | Laura | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 |
| A1 | ACURA OF BROOKFIELD | 5678 |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
When I get the list of all loans with the loan service
Then the result from the loan service should have 3 loans
And the first loan from the loan service has LoanId 3
And the loan with LoanId 1 has dealer name "SUBURBAN MOTORS HARLEY DAVIDSON"
And the loan with LoanId 2 has dealer name "HEISER CHEVROLET CADILLAC OF WEST BEND"
Scenario: GetByIdAsync Success
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 12 | 3 | Smith | Joe | | 1234 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | BB | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 45 | 6 | Jones | Laura | M | 5678 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message |
And the dealer table has these dealers
| Dealer Code | Name | DealerOrganizationNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 1234 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 5678 |
And loan types that look like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
| BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 |
When I get the loan with the loanId 2 from the loan service
Then the loan from the loan service has LoanId 2
And the loan from the loan service has LoanType.LoanComment "75% Reserve"
And the loan from the loan service has dealer name "HEISER CHEVROLET CADILLAC OF WEST BEND"
Scenario: GetByIdAsync - unknown loan type
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | BB | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | NC | 60 | 1450.2923 | 500 | false | | | Message |
And loan types that look like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When I get the loan with the loanId 1 from the loan service
Then the loan from the loan service has LoanId 1
And the loan from the loan service has LoanType.LoanComment ""
Scenario: GetByIdAsync Exception - Not Found
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 12 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 45 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
And loan types that look like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When I get the loan with the loanId 3 from the loan service
Then the Loan Service throws an exception with the message "Loan with id 3 not found."
Scenario: GetByIdAsync Exception - Loan Repository exception
Given the Loan Repository throws an exception in FindByIdAsync
When I get the loan with the loanId 2 from the loan service using the Mock Loan Repo
Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: Get Members - searching member number, don't return duplicates
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of members with search string "45"
Then the list of members returned is in this order
| Member Number |
| 451 |
| 452 |
Scenario: Get Members - searching member number, return in number order, not string order
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 4510 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of members with search string "45"
Then the list of members returned is in this order
| Member Number |
| 452 |
| 4510 |
Scenario: Get Members - only get numbers starting with the search string
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 245 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of members with search string "45"
Then the list of members returned is in this order
| Member Number |
| 451 |
Scenario: Get Members - not searching name
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate |Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 3 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 |NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 7 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 |BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of members with search string "Jo"
Then the list of members returned is in this order
| Member Number |
Scenario: Get Loan Numbers
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 731 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 73 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of loan numbers with search string "73"
Then the list of loan numbers looks like
| Loan Number |
| 73 |
| 731 |
Scenario: Get Loan Numbers, return in number order, not string order
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 731 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 7307 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of loan numbers with search string "73"
Then the list of loan numbers looks like
| Loan Number |
| 731 |
| 7307 |
Scenario: Get Loan Numbers - only get numbers starting with the search string
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 173 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 73 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of loan numbers with search string "73"
Then the list of loan numbers looks like
| Loan Number |
| 73 |
Scenario: Get Application Ids, return in number order
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 731 | 112 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | 334 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 73 | 35 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of application ids with search string "3"
Then the list of application ids looks like
|Application Id |
| 35 |
| 334 |
Scenario: Get Application Ids - only get ids starting with the search string
Given the Loan table has the following loans
| LoanId | Member Number | Loan Number | Application Id | Member Last Name | Member First Name | Member Initial | Dealer Code | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term | Reserve Amount | MonthlyPmt | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message |
| 1 | 452 | 173 | 133 | Smith | Joe | | S7 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | 12615.87 | 4.030 | 4.790 | NC | 63 | 210.0375 | 200 | false | | | Standard |
| 2 | 451 | 6 | 334 | Jones | Laura | M | H21 | 2/24/2021 | 2/27/2021 | 4/10/2021 | 3/10/2026 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
| 3 | 451 | 73 | 35 | Jones | Laura | M | H21 | 2/24/2023 | 2/27/2023 | 4/10/2023 | 3/10/2028 | 21274.24 | 2.490 | 4.970 | BB | 60 | 1450.2923 | 500 | false | | | Message |
When I get the list of application ids with search string "33"
Then the list of application ids looks like
|Application Id |
| 334 |
@@ -0,0 +1,77 @@
Feature: Loan Service - Saving
Save loan with Reserve, save loan with chargeback
Background:
Given dealers that look like
| Dealer Code | Name | DealerOrganizationNumber | DealerAccountNumber |
| S7 | SUBURBAN MOTORS HARLEY DAVIDSON | 3434 | 324324324324324 |
| H21 | HEISER CHEVROLET CADILLAC OF WEST BEND | 1111 | |
| | | 1234 | 924324324324324 |
And loan types that look like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
Scenario: Save with Reserve Success
Given I have these loans to save
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial |DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C |1234 | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 0 | false | 0.00 | | | New |
When I save the loan with the reserve
Then the loans from the loan service look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 566.98877654773011071101556050 | false | 0.00 | | Standard Reserve | New |
Scenario: Save with Reserve Failure
Given the Loan Repository throws an exception in UpdateAsync
And I have these loans to save
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 3/15/2021 | 3/12/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | | | false | 0.00 | | | New |
When I save the loan with the reserve with the mock loan repo
Then the Loan Service throws an exception with the message "Exception of type 'System.Exception' was thrown."
Scenario: Save with Reserve - Set ActionRequired - Dealer organization null
Given I have these loans to save
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial |DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | VIN Number | Make | Model | Year | Mileage | Color |
| 1 | 3250 | 144 | MUELLER | STEVEN | C |<null> | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 0 | false | 0.00 | | | New | M733P33389Y85216 | | | | | |
When I save the loan with the reserve
Then the loans from the loan service look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | | 2/10/2021 | 3/20/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 48 | 566.98877654773011071101556050 | false | 0.00 | | Standard Reserve | true | New |
Scenario: Save with Reserve - Set ActionRequired - first payment date before loan date
Given I have these loans to save
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | OriginalTermInDays |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 0 | false | 0.00 | | | New | 1491 |
When I save the loan with the reserve
Then the loans from the loan service look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 1234 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 584.06654602341792014440573200 | false | 0.00 | | Standard Reserve | true | New |
Scenario: Save with Reserve - Set ActionRequired True - Invalid Dealer Code
Given I have these loans to save
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | Loan Status | DealerOrganizationNumber | OriginalTermInDays |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 6222 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 0 | false | 0.00 | | | New | 2345 | 1491 |
When I save the loan with the reserve
Then the loans from the loan service look like
| ImportId | Member Number | Loan Number | Member Last Name | Member First Name | Member Initial | DealerOrganizationNumber | Loan Date | FirstPmtDate | FinalPmtDate | Posted Date | Loan Amount | Buy Rate | Contract Rate | MonthlyPmt | Loan Type Id | Term | Reserve Amount | Flat Fee Paid | Chargeback | Paid Loan Date | Reserve Message | ActionRequired | Loan Status |
| 1 | 3250 | 144 | MUELLER | STEVEN | C | 6222 | 2/10/2021 | 1/12/2021 | 2/10/2025 | 02/12/2021 | 25989.97 | 2.03 | 3.39 | 579.76 | NC | 50 | 584.06654602341792014440573200 | false | 0.00 | | Standard Reserve | true | New |
Scenario: SaveWithChargeback
Given I have these loans to save with chargebacks
| ImportId | Member Number | Loan Number | PaidLoanDate |
| 2 | 5841 | 147 | 2/19/2021 |
| 2 | 10094 | 143 | 12/22/2020 |
And the Loan table has the following loans
| LoanId | ImportId | Member Number | Loan Number | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Type Id | Term | Reserve Amount | Chargeback | Paid Loan Date | Reserve Message | Loan Status |
| 1 | 1 | 12 | 3 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | NC | 63 | 210.0375 | | | Standard | New |
| 2 | 1 | 5841 | 147 | 2/10/2021 | 2/27/2021 | 3/12/2021 | 2/12/2028 | NC | 84 | 1206.8106 | | | Message | New |
| 3 | 1 | 10094 | 143 | 12/7/2020 | 12/15/2020 | 1/21/2021 | 12/21/2025 | NC | 60 | 878.8602 | | | Message | New |
When I save the loans with chargebacks
And I get the list of all loans with the loan service
Then the loans in the loan table will look like
| LoanId | ImportId | Member Number | Loan Number | Loan Date | PostedDate | FirstPmtDate | FinalPmtDate | Loan Type Id | Term | Reserve Amount | Chargeback | Chargeback Message | Paid Loan Date | Reserve Message | Loan Status | Remaining Months at Close |
| 1 | 1 | 12 | 3 | 2/25/2021 | 2/28/2021 | 4/11/2021 | 6/11/2026 | NC | 63 | 210.0375 | | | | Standard | New | |
| 2 | 2 | 5841 | 147 | 2/10/2021 | 2/27/2021 | 3/12/2021 | 2/12/2028 | NC | 84 | 1206.8106 | 1206.8106 | Paid On or Before Chargeback Date | 2/19/2021 | Message | Closed | 84 |
| 3 | 2 | 10094 | 143 | 12/7/2020 | 12/15/2020 | 1/21/2021 | 12/21/2025 | NC | 60 | 878.8602 | 878.8602 | Paid On or Before Chargeback Date | 12/22/2020 | Message | Closed | 60 |
@@ -0,0 +1,436 @@
using FluentAssertions;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Moq;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
using TechTalk.SpecFlow.Assist.ValueRetrievers;
using Xunit;
namespace ReserveManagementSystemApi.Tests.ServiceTests
{
[Binding]
public sealed class LoanServiceSteps
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
private LoanService _loanService;
private AppDbContext _appDbContext;
private IEnumerable<Loan> _loansResult;
private List<Loan> _loansToSave;
private Loan _loanResult;
private Exception _exception;
private LoanFilter _loanFilter;
private List<string> _memberResult;
private List<string> _loanNumberResult;
private List<string> _applicationIdResult;
PaginatedListResponse _pagedListResponse = new PaginatedListResponse();
private IEnumerable<Loan> _closedLoans;
private IEnumerable<Loan> _loansInSystem;
private IEnumerable<Loan> _loansNotInSystem;
private Mock<ILoanRepository> _mockLoanRepo = new Mock<ILoanRepository>();
private Mock<IAuditHistoryRepository> _mockAuditHistRepo = new Mock<IAuditHistoryRepository>();
public LoanServiceSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
}
[BeforeScenario]
public void CreateDatabase()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_appDbContext = new AppDbContext(options);
_loanService = new LoanService(new LoanRepository(_appDbContext), new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock<ICoreApiServiceClient>().Object);
}
[AfterScenario]
public void Dispose()
{
_appDbContext.Database.EnsureDeleted();
_appDbContext.Dispose();
}
[Given(@"the Loan table has the following loans")]
public void GivenTheLoanTableHasTheFollowingLoans(Table table)
{
var loans = table.CreateSet<Loan>();
_appDbContext.Loans.AddRange(loans);
_appDbContext.SaveChanges();
}
[Given(@"I have these loans to save")]
public void GivenIHaveThisLoanToSave(Table table)
{
_loansToSave = table.CreateSet<Loan>().ToList();
}
[Given(@"a loan filter that looks like")]
public void GivenALoanFilterThatLooksLike(Table table)
{
_loanFilter = table.CreateInstance<LoanFilter>();
}
[Then(@"the first loan from the loan service has ImportId (.*)")]
public void ThenTheFirstLoanFromTheLoanServiceHasImportId(int importId)
{
Assert.Equal(importId, _loansResult.First().ImportId);
}
[Given(@"the Loan Repository throws an exception in ListAll")]
public void GivenTheLoanRepositoryThrowsAnExceptionInListAll()
{
_mockLoanRepo.Setup(a => a.GetAll()).Throws(new Exception());
_loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock<ICoreApiServiceClient>().Object);
}
[Given(@"the Loan Repository throws an exception in FindByIdAsync")]
public void GivenTheLoanRepositoryThrowsAnExceptionInFindByIdAsync()
{
_mockLoanRepo.Setup(a => a.FindByIdAsync(It.IsAny<int>())).Throws(new Exception());
_loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock<ICoreApiServiceClient>().Object);
}
[Given(@"the Loan Repository throws an exception in UpdateAsync")]
public void GivenTheLoanRepositoryThrowsAnExceptionInAdd()
{
_mockLoanRepo.Setup(a => a.UpdateAsync(It.IsAny<List<Loan>>())).Throws(new Exception());
_loanService = new LoanService(_mockLoanRepo.Object, new LoanTypeRepository(_appDbContext), new AuditHistoryService(_mockAuditHistRepo.Object), new Mock<ICoreApiServiceClient>().Object);
}
[Given(@"loan types that look like")]
public void GivenLoanTypesThatLookLike(Table table)
{
var loanTypes = table.CreateSet<LoanType>().ToList();
_appDbContext.LoanTypes.AddRange(loanTypes);
_appDbContext.SaveChanges();
}
[Given(@"I have these loans to save with chargebacks")]
public void GivenIHaveTheseLoansToSaveWithChargebacks(Table table)
{
_closedLoans = table.CreateSet<Loan>().ToList();
}
[Given(@"the dealer table has these dealers")]
public void GivenTheDealerTableHasTheseDealers(Table table)
{
var dealers = table.CreateSet<Dealer>().ToList();
_appDbContext.Dealers.AddRange(dealers);
_appDbContext.SaveChanges();
}
[When(@"I save the loans with chargebacks")]
public async void WhenISaveTheLoansWithChargebacks()
{
try
{
var result = await _loanService.SaveWithChargebackAsync(_closedLoans.ToList());
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of all loans with the loan service")]
public async void WhenIGetTheListOfAllLoansWithTheLoanService()
{
try
{
_loansResult = await _loanService.GetAllAsync();
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of all loans with the loan service with the mock loan repo")]
public async void WhenIGetTheListOfAllLoansWithTheLoanServiceWithTheMockLoanRepo()
{
try
{
_loansResult = await _loanService.GetAllAsync();
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of the filtered loans with the loan service")]
public async void WhenIGetTheListOfTheFilteredLoansWithTheLoanService()
{
try
{
_pagedListResponse = await _loanService.GetFilteredAsync(_loanFilter);
_loansResult = _pagedListResponse.LoanList;
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of paged loans")]
public async void WhenIGetTheListOfPagedLoans()
{
try
{
_pagedListResponse = await _loanService.GetFilteredAsync(_loanFilter);
_loansResult = _pagedListResponse.LoanList;
}
catch (Exception ex)
{
_exception = ex;
}
}
[Then(@"the result from the loan service should have (.*) pages")]
public void ThenTheResultFromTheLoanServiceShouldHavePages(int pageCount)
{
Assert.Equal(pageCount, _pagedListResponse.TotalPages);
}
[Then(@"the result from the loan service should have (.*) records")]
public void ThenTheResultFromTheLoanServiceShouldHaveRecords(int recordCount)
{
Assert.Equal(recordCount, _pagedListResponse.LoanList.Count());
}
[Then(@"the result from the loan service should have (.*) total items")]
public void ThenTheResultFromTheLoanServiceShouldHaveTotalItems(int recordCount)
{
Assert.Equal(recordCount, _pagedListResponse.TotalItems);
}
[Then(@"the first result from the loan service should have loanid (.*)")]
public void ThenTheFirstResultFromTheLoanServiceShouldHaveLoanId(int loanId)
{
Assert.Equal(loanId, _pagedListResponse.LoanList.First().LoanId);
}
//have to rethink this if we get rid of the other one
[When(@"I save the loan with the reserve")]
public async void WhenISaveTheLoan()
{
try
{
await _loanService.SaveLoansAsync(_loansToSave);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I save the loan with the reserve with the mock loan repo")]
public async void WhenISaveTheLoanWithTheReserveWithTheMockLoanRepo()
{
try
{
await _loanService.SaveLoansAsync(_loansToSave);
}
catch (Exception ex)
{
_exception = ex;
}
}
[Then(@"the result from the loan service has previous page should be (.*)")]
public void ThenTheResultFromTheLoanServiceHasPreviousPageShouldBe(bool hasPreviousPage)
{
Assert.Equal(hasPreviousPage, _pagedListResponse.HasPreviousPage);
}
[Then(@"the result from the loan service has next page should be (.*)")]
public void ThenTheResultFromTheLoanServiceHasNextPageShouldBe(bool hasNextPage)
{
Assert.Equal(hasNextPage, _pagedListResponse.HasNextPage);
}
[Then (@"the result from the loan service first page should be (.*)")]
public void ThenTheResultFromTheLoanServiceFirstPageShouldBe(int firstPage)
{
Assert.Equal(firstPage, _pagedListResponse.FirstPage);
}
[Then(@"the result from the loan service last page should be (.*)")]
public void ThenTheResultFromTheLoanServiceLastPageShouldBe(int lastPage)
{
Assert.Equal(lastPage, _pagedListResponse.LastPage);
}
[When(@"I get the loan with the loanId (.*) from the loan service")]
public async void WhenIGetTheLoanWithTheLoanIdFromTheLoanService(int loanId)
{
try
{
_loanResult = await _loanService.GetByIdAsync(loanId);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the loan with the loanId (.*) from the loan service using the Mock Loan Repo")]
public async void WhenIGetTheLoanWithTheLoanIdFromTheLoanServiceUsingTheMockLoanRepo(int loanId)
{
try
{
_loanResult = await _loanService.GetByIdAsync(loanId);
}
catch (Exception ex)
{
_exception = ex;
}
}
[When(@"I get the list of members with search string ""(.*)""")]
public async void WhenIGetTheListOfMembersWithSearchString(string searchString)
{
_memberResult = await _loanService.SearchMembers(searchString);
}
[When(@"I get the list of loan numbers with search string ""(.*)""")]
public async void WhenIGetTheListOfLoanNumbersWithSearchString(string searchString)
{
_loanNumberResult = await _loanService.SearchLoanNumbers(searchString);
}
[When(@"I get the list of application ids with search string ""(.*)""")]
public async void WhenIGetTheListOfApplicationIdsWithSearchString(string searchString)
{
_applicationIdResult = await _loanService.SearchApplicationIds(searchString);
}
[Then(@"the result from the loan service should have (.*) loans")]
public void ThenTheResultFromTheLoanServiceShouldHaveLoans(int loanCount)
{
Assert.Equal(loanCount, _loansResult.Count());
}
[Then(@"the list of members returned is in this order")]
public void ThenTheListOfMembersReturnedIsInThisOrder(Table table)
{
var expectedMemberNumbers = table.Rows.Select(x => x[0]).ToList();
_memberResult.Should().BeEquivalentTo(expectedMemberNumbers).And.ContainInOrder(expectedMemberNumbers);
}
[Then(@"the list of loan numbers looks like")]
public void ThenTheListOfLoanNumbersLooksLike(Table table)
{
var expectedLoanNumbers = table.Rows.Select(x => x[0]).ToList();
_loanNumberResult.Should().BeEquivalentTo(expectedLoanNumbers).And.ContainInOrder(expectedLoanNumbers);
}
[Then(@"the list of application ids looks like")]
public void ThenTheListOfApplicationIdsLooksLike(Table table)
{
var expectedApplicationIds = table.Rows.Select(x => x[0]).ToList();
_applicationIdResult.Should().BeEquivalentTo(expectedApplicationIds).And.ContainInOrder(expectedApplicationIds);
}
[Then(@"the loans from the loan service look like")]
public async void ThenTheLoanFromTheLoanServiceLooksLike(Table table)
{
var actualLoans = (await _loanService.GetAllAsync()).ToList();
table.CompareToSet(actualLoans);
}
[Then(@"the first loan from the loan service has LoanId (.*)")]
public void ThenTheFirstLoanFromTheLoanServiceHasLoanId(int loanId)
{
Assert.Equal(loanId, _loansResult.First().LoanId);
}
[Then(@"the loan ids from the loan service look like")]
public void ThenTheLoanIdsFromTheLoanServiceLookLike(Table table)
{
var expectedLoanIds = table.CreateSet<int>().ToList();
_loansResult.Select(x => x.LoanId).Should().BeEquivalentTo(expectedLoanIds).And.ContainInOrder(expectedLoanIds);
}
[Then(@"the loan ids from the loan service look like ""(.*)""")]
public void ThenTheLoanIdsFromTheLoanServiceLookLike(string listOfIds)
{
var expected = listOfIds.Split(',').Select(x => int.Parse(x)).ToList();
_loansResult.Select(x => x.LoanId).Should().BeEquivalentTo(expected);
}
[Then(@"the loan with LoanId (.*) has dealer name ""(.*)""")]
public void ThenTheLoanWithLoanIdHasDealerName(int loanId, string dealerName)
{
Assert.Equal(dealerName, _loansResult.First(x => x.LoanId == loanId).Dealer.Name);
}
[Then(@"the Loan Service throws an exception with the message ""(.*)""")]
public void ThenTheLoanServiceThrowsAnExceptionWithTheMessage(string message)
{
Assert.NotNull(_exception);
Assert.Equal(message, _exception.Message);
}
[Then(@"the loan from the loan service has LoanId (.*)")]
public void ThenTheLoanFromTheLoanServiceHasLoanId(int loanId)
{
Assert.Equal(loanId, _loanResult.LoanId);
}
[Then(@"the loan from the loan service has dealer name ""(.*)""")]
public void ThenTheLoanFromTheLoanServiceHasDealerName(string dealerName)
{
Assert.Equal(dealerName, _loanResult.Dealer.Name);
}
[Then(@"the flat fee result for MemberNumber (.*) should be (.*)")]
public void ThenTheReserveAmountResultShouldBe(string member, bool feePaid)
{
Assert.Equal(feePaid, _loansResult.FirstOrDefault(x => x.MemberNumber == member).FlatFeePaid);
}
[Then(@"the loan from the loan service has LoanType.LoanComment ""(.*)""")]
public void ThenTheLoanFromTheLoanServiceHasLoanTypeDescription(string description)
{
Assert.Equal(description, _loanResult.LoanType?.LoanComment ?? string.Empty);
}
[Then(@"the loans in the system look like")]
public void ThenTheLoansInTheSystemLookLike(Table table)
{
table.CompareToSet(_loansInSystem);
}
[Then(@"the loans not in the system look like")]
public void ThenTheLoansNotInTheSystemLookLike(Table table)
{
table.CompareToSet(_loansNotInSystem);
}
[Then(@"the loans in the loan table will look like")]
public void ThenTheLoansInTheLoanTableWillLookLike(Table table)
{
table.CompareToSet(_loansResult);
}
}
}
@@ -0,0 +1,230 @@
Feature: Reserve Calculation
Calculates reserve amount for a loan
Scenario: Calculate Reserve with Standard Markup
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | NC | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 118.73687752592344707611833965
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Boat Reserve with Standard Markup
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType |
| 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.50 | 3.00 | NC | 24 | DL09 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 310.01
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup and Days to First Payment is less than 30
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 2/27/2021 | 3/27/2021 | 2/27/2028 | 55799.16 | 4.59 | 6.34 | NC | 84 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 2917.8694562514364202612072482
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup - first pay date is less than 30 days from loan date (this calculation might be wrong)
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 4/23/2021 | 5/21/2021 | 4/21/2026 | 6474.80 | 3.24 | 5.24 | NC | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 263.93215613213476391531161260
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup - loan type id is missing, but will calculate the same as NC
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term |
| 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 118.73687752592344707611833965
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup - BB
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term |
| 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 1.48 | 3.89 | BB | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| BB | Standard | S | 0 | 0 | 0 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 158.3158367012312627681577862
And the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup - NR
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type Id | Term |
| 5/28/2015 | 6/27/2015 | 5/27/2017 | 6200.20 | 3.89 | 3.89 | NR | 24 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NR | No reserve to be paid | N | 0 | 0 | 0 | 0.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 0
And the reserve message should be No Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Standard Markup Flat Fee Applied
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 2/18/2021 | 4/4/2021 | 3/1/2024 | 12000.95 | 2.89 | 3.49 | NC | 36 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 90.007125
And the reserve message should be Flat Fee Reserve
And FlatFeePaid should be "true"
Scenario: Calculate Reserve with No Markup Flat Fee Applied
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 7/10/2019 | 8/24/2019 | 7/24/2024 | 20500 | 3.49 | 3.49 | NC | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 153.75
Then the reserve message should be Flat Fee Reserve
And FlatFeePaid should be "true"
Scenario: Calculate Boat Reserve with No Markup Standard Reserve Applied @ 1% Reserve
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType |
| 7/10/2019 | 8/24/2019 | 7/24/2024 | 20500 | 3.49 | 3.49 | NC | 60 | DL12 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 205
Then the reserve message should be Standard Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Boat Reserve with Non Standard Markup
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType |
| 1/2/2021 | 2/16/2021 | 1/26/2026 | 28423.05 | 1.83 | 11.73 | NC | 60 | DL10 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 1705.383
Then the reserve message should be Standard Reserve - Exceeds Max Markup
And FlatFeePaid should be "false"
Scenario: Calculate Reserve with Non Standard Markup
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 1/2/2021 | 2/16/2021 | 1/26/2026 | 28423.05 | 1.83 | 11.73 | NC | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 6104.6234279062733014747185835
Then the reserve message should be Standard Reserve - Exceeds Max Markup
And FlatFeePaid should be "false"
Scenario: Calculate Buydown Reserve
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 4/19/2018 | 5/19/2018 | 4/19/2024 | 27042.25 | 4.6 | 1.9 | NC | 72 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be -2362.679817869475221821891298
Then the reserve message should be Buy Down Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Boat Buydown Reserve
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType |
| 4/19/2018 | 5/19/2018 | 4/19/2024 | 27042.25 | 4.6 | 1.9 | NC | 72 | DL11 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be -2362.679817869475221821891298
Then the reserve message should be Buy Down Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Buydown Reserve - 0.5 difference
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 5/15/2019 | 7/1/2019 | 6/1/2022 | 6261 | 5.27 | 4.77 | NC | 36 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be -52.284980016715778985051587237
Then the reserve message should be Buy Down Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Boat Buydown Reserve - 0.05 difference (reserve should be 0)
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term | AccountType |
| 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 9.95 | 9.9 | NC | 60 | DL09 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 0
Then the reserve message should be No Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Buydown Reserve - 0.05 difference (reserve should be 0)
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 9.95 | 9.9 | NC | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 0
Then the reserve message should be No Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Buydown Reserve - 0.25 difference (reserve should be 0)
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | Loan Amount | Buy Rate | Contract Rate | Loan Type | Term |
| 8/4/2017 | 9/3/2017 | 8/3/2022 | 15721.42 | 8.25 | 8 | NC | 60 |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 0
Then the reserve message should be No Reserve
And FlatFeePaid should be "false"
Scenario: Calculate Reserve when there are weird payment dates
Given a loan that looks like
| Loan Date | First Pmt Date | Final Pmt Date | PostedDate | Loan Amount | Buy Rate | Contract Rate | Loan Type |
| 6/14/2021 | 7/24/2021 | 6/30/2021 | 6/18/2021 | 20500 | 3.49 | 3.49 | NC |
And a loan type table that looks like
| LoanTypeId | LoanComment | ChargebackMethod | NoChargebackDays | NoChargebackPayments | ReserveDiscount | FlatFeePercent | NoReserveThreshold |
| NC | 75% Reserve | N | 90 | 3 | 25.00 | 1.00 | 0.25 |
When the reserve is calculated
Then the reserve amount should be 0
Then the reserve message should be Exception while calculating reserve
And FlatFeePaid should be "false"
@@ -0,0 +1,129 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SpecFlowObsoleteCodeBehindFiles Remove="Features\ImportReadService Closed - Copy.feature.cs" />
<SpecFlowObsoleteCodeBehindFiles Remove="Features\ImportReadService New - Copy.feature.cs" />
<SpecFlowObsoleteCodeBehindFiles Remove="Features\LoanService - Copy.feature.cs" />
<SpecFlowObsoleteCodeBehindFiles Remove="Features\LoanService - Filtering and Pagination - Copy.feature.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="1.4.1" />
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="LCU.Library.Internal.ApiSecurity" Version="1.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.18" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="SpecFlow" Version="3.9.22" />
<PackageReference Include="SpecFlow.xUnit" Version="3.9.22" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReserveManagementSystemApi\ReserveManagementSystemApi.csproj" />
<ProjectReference Include="..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Features\ImportReadService Closed from DNA.feature.cs">
<DependentUpon>ImportReadService Closed from DNA.feature</DependentUpon>
</Compile>
<Compile Update="Features\ImportReadService Closed.feature.cs">
<DependentUpon>ImportReadService Closed.feature</DependentUpon>
</Compile>
<Compile Update="Features\ImportReadService New from DNA.feature.cs">
<DependentUpon>ImportReadService New from DNA.feature</DependentUpon>
</Compile>
<Compile Update="Features\ImportReadService New.feature.cs">
<DependentUpon>ImportReadService New.feature</DependentUpon>
</Compile>
<Compile Update="Features\ImportService.feature.cs">
<DependentUpon>ImportService.feature</DependentUpon>
</Compile>
<Compile Update="Features\LoanService - Saving.feature.cs">
<DependentUpon>LoanService - Saving.feature</DependentUpon>
</Compile>
<Compile Update="Features\LoanService - Filtering and Pagination.feature.cs">
<DependentUpon>LoanService - Filtering and Pagination.feature</DependentUpon>
</Compile>
<Compile Update="Features\LoanService - Get All and Get By Id.feature.cs">
<DependentUpon>LoanService - Get All and Get By Id.feature</DependentUpon>
</Compile>
<Compile Update="Features\ImportReadServiceSteps.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Features\ImportServiceSteps.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Features\LoanServiceSteps.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Features\LoanCalculationServiceSteps.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Features\ChargebackCalculation.feature.cs">
<DependentUpon>ChargebackCalculation.feature</DependentUpon>
</Compile>
<Compile Update="Features\ReserveCalculation.feature.cs">
<DependentUpon>ReserveCalculation.feature</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<SpecFlowFeatureFiles Update="Features\ImportReadService Closed from DNA.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\ImportReadService Closed.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\ImportReadService New from DNA.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\ImportReadService New.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\ImportService.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\ChargebackCalculation.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\LoanService - Saving.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
<SpecFlowFeatureFiles Update="Features\LoanService - Filtering and Pagination.feature">
<Visible>$(UsingMicrosoftNETSdk)</Visible>
<CodeBehindFile>%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)</CodeBehindFile>
</SpecFlowFeatureFiles>
</ItemGroup>
</Project>
@@ -0,0 +1,3 @@
{
"parallelizeTestCollections": false
}
@@ -0,0 +1,11 @@
namespace ReserveManagementSystemApi.Configurations
{
public class ApiConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public string[] CorsAllowedOrigins { get; set; }
public FeatureFlags FeatureFlags { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace ReserveManagementSystemApi.Configurations
{
public class FeatureFlags
{
public bool RunRmsActiveLoanConsumer { get; set; }
public bool EnableImportExportActive { get; set; }
public bool EnableImportExportClosed { get; set; }
}
}
@@ -0,0 +1,9 @@
using LCU.Library.Internal.LcuMassTransitModels;
namespace ReserveManagementSystemApi.Configurations
{
public class ReceiveEndpointConfigurations
{
public ReceiveEndpointConfiguration RmsActiveLoanReceiveEndpoint { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace ReserveManagementSystemApi.Configurations
{
public class SqlConnectionConfiguration
{
public string ConnectionString { get; set; }
public int MaxRetryCount { get; set; }
public TimeSpan MaxRetryDelay { get; set; }
}
}
@@ -0,0 +1,40 @@
using MassTransit;
using LCU.Api.Internal.MaidBotLib.Models;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.Consumers;
public class RmsActiveImportConsumer : IConsumer<RmsActiveImportRequest>
{
private readonly IServiceScopeFactory _scopeFactory;
public RmsActiveImportConsumer(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task Consume(ConsumeContext<RmsActiveImportRequest> context)
{
try
{
Log.Debug("Starting RmsActiveImportConsumer.");
using var scope = _scopeFactory.CreateScope();
await Task.Run(async () => await scope.ServiceProvider.GetRequiredService<IRmsActiveLoanService>().HandleRmsActiveImportRequest(context.Message));
Log.Debug("Ending RmsActiveImportConsumer.");
}
catch (ServiceException)
{
throw;
}
catch (Exception exception)
{
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Fatal(exception, $"Unexpected RmsActiveImportConsumer Failure. " +
$"The RmsActiveImportRequest is placed on ReserveManagementSystemApi.ActiveImport_error.");
throw;
}
}
}
@@ -0,0 +1,34 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuditHistoryController : ControllerBase
{
private readonly IAuditHistoryService _auditHistoryService;
public AuditHistoryController(IAuditHistoryService auditHistoryService)
{
_auditHistoryService = auditHistoryService;
}
[HttpGet(Name = "GetAllHistory")]
[ProducesResponseType(typeof(IEnumerable<AuditHistory>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _auditHistoryService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Audit Histories");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,51 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DealerController : ControllerBase
{
private readonly IDealerService _dealerService;
public DealerController(IDealerService dealerService)
{
_dealerService = dealerService;
}
[HttpGet("Search/{dealerSearchText}", Name = "Search")]
[ProducesResponseType(typeof(IEnumerable<Dealer>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Search(string dealerSearchText)
{
try
{
var result = await _dealerService.Search(dealerSearchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning dealers");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetByDealerCode/{dealerCode}", Name = "GetDealerByCode")]
[ProducesResponseType(typeof(Dealer), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetByDealerCode(string dealerCode)
{
try
{
var result = await _dealerService.GetDealerByCodeAsync(dealerCode);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning dealer");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,183 @@
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemApi.Services.QueueServices;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ImportController : ControllerBase
{
private readonly IImportReadService _importReadService;
private readonly IImportService _importService;
private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService;
public ImportController(IImportReadService importReadService, IImportService importService, IQueueRmsActiveLoanService queueRmsActiveLoanService)
{
_importReadService = importReadService;
_importService = importService;
_queueRmsActiveLoanService = queueRmsActiveLoanService;
}
[HttpGet(Name = "GetAllImports")]
[ProducesResponseType(typeof(IEnumerable<Import>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _importService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Imports");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetRecentImports/{numberOfImports}", Name = "GetRecentImports")]
[ProducesResponseType(typeof(IEnumerable<Import>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetRecent(int numberOfImports)
{
try
{
var result = await _importService.GetRecentAsync(numberOfImports);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Imports");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetById/{importId}", Name = "GetImportById")]
[ProducesResponseType(typeof(Loan), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetById(int importId)
{
try
{
var result = await _importService.GetByIdAsync(importId);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan with id {importId}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportNewLoanFile", Name ="ImportNewLoanFile")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportNewLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadNewLoanImportFile(stream, file.FileName, DateTime.Now);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import new loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportBoatLoanFile", Name = "ImportBoatLoanFile")]
[ProducesResponseType(typeof(List<ImportBoatLoan>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportBoatLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadBoatLoanImportFile(stream);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import boat loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("ImportClosedLoanFile", Name = "ImportClosedLoanFile")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoanFile(IFormFile file)
{
try
{
var stream = new StreamReader(file.OpenReadStream());
var import = await _importReadService.ReadClosedLoanImportFile(stream, file.FileName, DateTime.Now);
return Ok(import);
}
catch (Exception e)
{
Log.Error("Failure to import closed loan file, message: {0} {1}", e.Message, e.StackTrace);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportActiveLoans", Name = "ImportActiveLoans")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportActiveLoans(string? newerThanDateTime = "", bool skipExport = false)
{
try
{
var request = new RmsActiveImportRequest()
{
ImportDateTime = newerThanDateTime,
SkipExport = skipExport
};
await _queueRmsActiveLoanService.QueueRmsActiveLoanServiceRequest(request);
return Accepted();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Active Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportClosedLoans", Name = "ImportClosedLoans")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoans(string? newerThanDateTime)
{
try
{
var import = await _importReadService.ImportClosedLoansFromDNA(newerThanDateTime);
return Ok(import);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Closed Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPut("ImportClosedLoansWithUpdates", Name = "ImportClosedLoansWithUpdates")]
[ProducesResponseType(typeof(Import), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> ImportClosedLoansWithUpdates(string? newerThanDateTime, bool skipExport = false)
{
try
{
var import = await _importReadService.ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, skipExport);
return Ok(import);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
}
@@ -0,0 +1,146 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoanController : ControllerBase
{
private readonly ILoanService _loanService;
public LoanController(ILoanService loanService)
{
_loanService = loanService;
}
[HttpGet(Name = "GetAllLoans")]
[ProducesResponseType(typeof(IEnumerable<Loan>), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Get()
{
try
{
var result = await _loanService.GetAllAsync();
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("SearchLoans", Name = "SearchLoans")]
[ProducesResponseType(typeof(PaginatedListResponse), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> Post([FromBody]LoanFilter filter)
{
try
{
var result = await _loanService.GetFilteredAsync(filter);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, "Failure returning Filtered Loans");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchMembers/{searchText}", Name = "SearchMembers")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchMembers(string searchText)
{
try
{
var result = await _loanService.SearchMembers(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning members");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchLoanNumbers/{searchText}", Name = "SearchLoanNumbers")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchLoanNumbers(string searchText)
{
try
{
var result = await _loanService.SearchLoanNumbers(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan numbers");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("SearchApplicationIds/{searchText}", Name = "SearchApplicationIds")]
[ProducesResponseType(typeof(string), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> SearchApplicationIds(string searchText)
{
try
{
var result = await _loanService.SearchApplicationIds(searchText);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning application ids");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpGet("GetById/{loanId}", Name = "GetLoanById")]
[ProducesResponseType(typeof(Loan), 200)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetById(int loanId)
{
try
{
var result = await _loanService.GetByIdAsync(loanId);
return Ok(result);
}
catch (Exception e)
{
Log.Error(e, $"Failure returning loan with id {loanId}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[HttpPost("CalculateReserve", Name = "CalculateReserve")]
public async Task<LoanReserve> Post([FromBody] LoanReserveInput input)
{
try
{
return await _loanService.GetLoanReserveAsync(input);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace);
return null;
}
}
[HttpPost("CalculateChargeback", Name = "CalculateChargeback")]
public async Task<LoanChargeback> Post([FromBody] LoanChargebackInput input)
{
try
{
return await _loanService.GetLoanChargebackAsync(input);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace);
return null;
}
}
}
}
@@ -0,0 +1,33 @@
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class StatusController : ControllerBase
{
private readonly IStatusService _statusService;
public StatusController(IStatusService statusService)
{
_statusService = statusService;
}
[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
return Ok(_statusService.GetEnviromentView());
}
catch (ServiceException exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, exception.Message);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,10 @@
namespace ReserveManagementSystemApi.Enums
{
public enum ActivityType
{
Warning,
Error,
LoanStatusChange,
NewLoanUpdate
}
}
@@ -0,0 +1,10 @@
namespace ReserveManagementSystemApi.Enums
{
public enum LoanStatus
{
New,
Updated,
Closed,
MissingActiveLoan
}
}
@@ -0,0 +1,27 @@
namespace ReserveManagementSystemApi.Helpers
{
public static class BatchHelper
{
public static IEnumerable<IEnumerable<TSource>> CreateBatch <TSource>(
List<TSource> source, int batchSize)
{
var items = new TSource[batchSize];
var count = 0;
foreach (var item in source)
{
items[count++] = item;
if (count == batchSize)
{
yield return items;
items = new TSource[batchSize];
count = 0;
}
}
if (count > 0)
{
yield return items.Take(count);
}
}
}
}
@@ -0,0 +1,56 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class BoatLoanCalculationHelper
{
private LoanReserve loanReserve = new LoanReserve();
public BoatLoanCalculationHelper()
{
}
public LoanReserve CalculateStandardMarkup(CalculationConfig cc)
{
var markup = cc.LoanData().ContractRate - cc.LoanData().BuyRate;
loanReserve.ReserveTypeMessage = "Standard Reserve";
switch (markup)
{
case decimal d when (d >= (decimal)0.00 && d <= (decimal)0.49):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).01;
break;
case decimal d when (d >= (decimal)0.50 && d <= (decimal)0.74):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).02;
break;
case decimal d when (d >= (decimal)0.75 && d <= (decimal)0.99):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).03;
break;
case decimal d when (d >= (decimal)1.00 && d <= (decimal)1.49):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).04;
break;
case decimal d when (d >= (decimal)1.50 && d <= (decimal)1.99):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).05;
break;
case decimal d when (d == (decimal)2.00):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06;
break;
case decimal d when (d > (decimal)2.00):
loanReserve.ReserveAmount = cc.LoanData().LoanAmount * (decimal).06;
loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup";
break;
default:
loanReserve.ReserveAmount = 0;
loanReserve.ReserveTypeMessage = "No Reserve";
break;
}
return loanReserve;
}
internal DateTime GetLastChargebackDate (DateTime firstPmtDate, int numOfChargebackPayments)
{
return firstPmtDate.AddMonths(numOfChargebackPayments - 1);
}
}
}
@@ -0,0 +1,210 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class ImportHelper
{
private string _firstName;
private string _middleInitial;
private const double MonthInDays = 30.4375;
public ImportHelper()
{
}
public List<Loan> TransformNewLoan(List<Models.ImportNewLoan> iImportLoans, int iImportId)
{
var result = new List<Loan>();
foreach (var iLoan in iImportLoans)
{
Loan loan = new Loan();
loan.ImportId = iImportId;
loan.MemberNumber = iLoan.MemberNumber;
loan.LoanNumber = iLoan.LoanNumber;
SplitFirstMiddleName(iLoan.FirstAndMiddleName);
loan.MemberFirstName = _firstName?.ToUpper();
loan.MemberInitial = _middleInitial?.ToUpper();
loan.MemberLastName = iLoan.LastName?.ToUpper();
loan.DealerOrganizationNumber = iLoan.DealerOrganizationNumber;
loan.LoanDate = iLoan.LoanDate;
loan.PostedDate = iLoan.PostedDate;
loan.FirstPmtDate = iLoan.FirstPmtDate;
loan.FinalPmtDate = iLoan.FinalPmtDate;
loan.LoanAmount = iLoan.LoanAmount;
loan.BuyRate = Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero);
loan.ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero);
loan.MonthlyPmt = iLoan.MonthlyPmt;
loan.LoanTypeId = iLoan.LoanType;
loan.ChargeBack = null;
loan.PaidLoanDate = null;
loan.ApplicationId = iLoan.ApplicationId;
loan.AccountType = iLoan.AccountType;
loan.Gap = !string.IsNullOrEmpty(iLoan.Gap) && Convert.ToBoolean(iLoan.Gap);
loan.Warranty = !string.IsNullOrEmpty(iLoan.Warranty) && Convert.ToBoolean(iLoan.Warranty);
loan.CreditLife = !string.IsNullOrEmpty(iLoan.CreditLife) && Convert.ToBoolean(iLoan.CreditLife);
loan.CreditDisability = !string.IsNullOrEmpty(iLoan.CreditDisability) && Convert.ToBoolean(iLoan.CreditDisability);
loan.Mbp = !string.IsNullOrEmpty(iLoan.Mbp) && Convert.ToBoolean(iLoan.Mbp);
loan.Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0);
Vehicle vehicle = new Vehicle();
vehicle.VINNumber = !string.IsNullOrEmpty(iLoan.VINNumber) ? iLoan.VINNumber : null;
vehicle.Make = iLoan.Make;
vehicle.Model = iLoan.Model;
vehicle.Year = iLoan.Year;
vehicle.Mileage = ValidateMileage(iLoan.Mileage);
vehicle.Color = iLoan.Color;
loan.Vehicle = vehicle;
result.Add(loan);
}
return result;
}
public List<Loan> TransformLoanFromDNA(List<IndirectLoanDetail> importLoans, int iImportId)
{
var result = new List<Loan>();
if (!importLoans.Any())
{
return result;
}
foreach (var iLoan in importLoans)
{
Loan loan = new Loan
{
ImportId = iImportId,
MemberNumber = iLoan.MemberNumber,
LoanNumber = iLoan.LoanNumber,
MemberFirstName = iLoan.MemberFirstName?.ToUpper(),
MemberInitial = iLoan.MemberInitial?.ToUpper(),
MemberLastName = iLoan.MemberLastName?.ToUpper(),
DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty,
LoanDate = iLoan.LoanDate,
FirstPmtDate = iLoan.FirstPmtDate,
FinalPmtDate = iLoan.FinalPmtDate,
LoanAmount = iLoan.LoanAmount,
BuyRate = (iLoan.BuyRate != 0) ? Math.Round(iLoan.BuyRate, 3, MidpointRounding.AwayFromZero) : 0,
ContractRate = Math.Round(iLoan.ContractRate, 3, MidpointRounding.AwayFromZero),
MonthlyPmt = iLoan.MonthlyPmt,
LoanTypeId = iLoan.LoanType,
ApplicationId = iLoan.ApplicationId,
Gap = iLoan.GAP,
Warranty = iLoan.Warranty,
CreditLife = iLoan.CreditLife,
CreditDisability = iLoan.CreditDisability,
Mbp = iLoan.Mbp,
AccountType = iLoan.AccountType,
PaidLoanDate = iLoan.PaidLoanDate,
Term = (int)Math.Round(Convert.ToDecimal(iLoan.OriginalTermInDays) / (decimal)MonthInDays, 0)
};
if (iLoan.PostedDate == DateTime.MinValue)
{
loan.PostedDate = iLoan.LoanDate;
}
else
{
loan.PostedDate = iLoan.PostedDate;
}
loan.Vehicle = new Vehicle
{
VINNumber = !string.IsNullOrEmpty(iLoan.Vin) ? iLoan.Vin : null,
Make = iLoan.Make,
Model = iLoan.Model,
Year = iLoan.Year.ToString(),
Mileage = ValidateMileage(iLoan.Mileage.ToString()),
Color = iLoan.Color
};
loan.Dealer = new Dealer
{
DealerCode = (!String.IsNullOrWhiteSpace(iLoan.DealerCode)) && (iLoan.DealerCode.Length <= 4) ? iLoan.DealerCode : string.Empty,
Name = (!String.IsNullOrWhiteSpace(iLoan.DealerName)) ? iLoan.DealerName : string.Empty,
DealerAccountNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerAccountNumber)) ? iLoan.DealerAccountNumber : string.Empty,
DealerOrganizationNumber = (!String.IsNullOrWhiteSpace(iLoan.DealerOrganizationNumber)) ? iLoan.DealerOrganizationNumber : string.Empty
};
result.Add(loan);
}
return result;
}
public List<Loan> TransformBoatLoan(List<ImportBoatLoan> importLoans)
{
var result = new List<Loan>();
if (!importLoans.Any())
{
return result;
}
foreach (var iLoan in importLoans)
{
Loan loan = new Loan
{
LoanNumber = iLoan.LoanNumber,
ReserveAmount = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "FALSE") ? iLoan.Amount : 0,
ChargeBack = (!String.IsNullOrWhiteSpace(iLoan.IsChargeback) && iLoan.IsChargeback.ToUpper() == "TRUE") ? iLoan.Amount : 0,
Adjustment = (!String.IsNullOrWhiteSpace(iLoan.IsAdjustment) && iLoan.IsAdjustment.ToUpper() == "TRUE") ? iLoan.Amount : 0,
AccountType = iLoan.AccountType
};
loan.Dealer = new Dealer
{
DealerAccountNumber = iLoan.DealerAccountNumber
};
result.Add(loan);
}
return result;
}
private double ValidateMileage(string mileage)
{
double convertedMileage;
if (double.TryParse(mileage, out convertedMileage))
{
return convertedMileage;
}
return convertedMileage;
}
public IEnumerable<Loan> TransformClosedLoans(List<ClosedIndirectLoan> importLoans, int importId)
{
return importLoans.Select(x => new Loan
{
ImportId = importId,
LoanNumber = x.LoanNumber,
PaidLoanDate = x.PaidLoanDate
//will update LoanStatus when saving
});
}
private void SplitFirstMiddleName(string name)
{
//initial first name last name rules with first name character max length set in DB as 20, and middle initial max as 1
_middleInitial = String.Empty;
_firstName = name;
string[] names = name.Trim().Split(" ");
if (names != null && names.Length > 1) //when true there is a middle initial
{
_firstName = _firstName.Substring(0, name.Length - names.Last().Length).Trim();
_middleInitial = names.Last().Substring(0, 1);
}
else
{
_firstName = name; //no middle initial
}
}
}
}
@@ -0,0 +1,120 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Helpers
{
public class LoanCalculationHelper
{
private decimal _daysInterest;
private LoanReserve loanReserve = new LoanReserve();
public LoanCalculationHelper()
{
}
public LoanReserve CalculateBuyDown(CalculationConfig cc)
{
// Case - Buy down Below Threshold where difference in rates is less than or equal to 0.25 percent
if (cc.BuydownRateDifference() <= cc.NoReserveThreshold()) //we already know it's positive
{
loanReserve.ReserveAmount = 0;
loanReserve.ReserveTypeMessage = "No Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
// Case - This is a regular Buy Down
_daysInterest = cc.BuydownRateDifference() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = -(cc.BuyFinanceCharge - cc.ContractFinanceCharge + _daysInterest);
loanReserve.ReserveTypeMessage = "Buy Down Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
public LoanReserve CalculateNoMarkup(CalculationConfig cc)
{
// Case - Contract rate is equal to the Buy rate
_daysInterest = cc.StdRate() * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = (cc.ContractFinanceCharge - cc.BuyFinanceCharge + _daysInterest);
loanReserve.ReserveTypeMessage = "No Reserve";
loanReserve.FlatFeePaid = false;
return loanReserve;
}
public LoanReserve CalculateStandardMarkup(CalculationConfig cc)
{
// Case - Contract rate is greater than Buy rate
_daysInterest = (1 - cc.ReserveDiscount()/100) * cc.StdRate() / 100 * cc.DaysToFirstPmt() * cc.LoanData().LoanAmount /
365 * (decimal)Math.Pow(1 + (double)cc.LoanData().ContractRate / 100, (double)cc.LoanData().Term / 12);
loanReserve.ReserveAmount = ((1 - cc.ReserveDiscount() / 100) * (cc.ContractFinanceCharge - cc.BuyFinanceCharge) + _daysInterest);
if ((double)cc.StdRate() <= 2.5)
{
loanReserve.ReserveTypeMessage = "Standard Reserve";
}
else
{
loanReserve.ReserveTypeMessage = "Standard Reserve - Exceeds Max Markup";
}
return loanReserve;
}
public LoanReserve AssignFlatFee(LoanReserve loanReserve, CalculationConfig cc)
{
loanReserve.FlatFeePaid = false;
if (loanReserve.ReserveAmount < (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100)))
{
loanReserve.ReserveAmount = (cc.FlatFeePercent() / 100 * cc._loanData.LoanAmount * (1 - cc.ReserveDiscount() / 100));
loanReserve.ReserveTypeMessage = "Flat Fee Reserve";
loanReserve.FlatFeePaid = true;
}
return loanReserve;
}
public decimal FinanceCharge(int term, decimal loanAmount, decimal rate)
{
if (rate == 0) { return 0; }
double paymentRate = (double)rate / (double)1200;
decimal rateFactor = (decimal)Math.Pow((1 + paymentRate), (double)term);
return (loanAmount * term * rate / 1200 * rateFactor) / (rateFactor - 1) - loanAmount;
}
internal DateTime GetLastChargebackDate(DateTime loanDate, DateTime firstPmtDate, int numOfChargebackPayments, Int16? numOfChargebackDays)
{
// in rare cases that date from loan date is most recent, check table values and compare between loan date vs. first payment date
int result = DateTime.Compare(loanDate.AddDays((Double)numOfChargebackDays), firstPmtDate.AddMonths(numOfChargebackPayments - 1));
if (result > 0)
{
return loanDate.AddDays((Double)numOfChargebackDays); //Loan Date is most recent
}
return firstPmtDate.AddMonths(numOfChargebackPayments - 1);
}
internal decimal CalculateChargeback(decimal reserve, int term, int remainingMonths)
{
return reserve * MonthFactor(remainingMonths) / MonthFactor(term);
}
private decimal MonthFactor(int numberOfMonths)
{
return numberOfMonths * (numberOfMonths + 1) / 2;
}
public int GetRemainingMonths(Loan loanData)
{
if (loanData.PaidLoanDate >= loanData.FinalPmtDate)
return 0;
var monthsOccurred = (loanData.PaidLoanDate.Value - loanData.FirstPmtDate).TotalDays / (365) * 12 + 1;
var remainingMonths = (int)(loanData.Term - Math.Round((decimal)monthsOccurred, 0));
return remainingMonths;
}
}
}
@@ -0,0 +1,72 @@
using NCrontab;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.HostedService
{
public class HostedImportExportActive : BackgroundService
{
private readonly IConfiguration _config;
private readonly IServiceScopeFactory _scopeFactory;
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public const string DefaultCronPattern = "0 19 * * 0-6";
private CancellationToken _cancellationToken;
public HostedImportExportActive(IConfiguration config, IServiceScopeFactory scopeFactory)
{
_config = config;
_scopeFactory = scopeFactory;
try
{
_schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportActiveCronSchedule").Value);
}
catch (Exception ex)
{
Log.Error(ex, "Failure parsing crontab in appsettings.");
_schedule = CrontabSchedule.Parse(DefaultCronPattern);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Log.Information("Hosted ImportExportActive is starting.");
Log.Information($"The import export active job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}");
_cancellationToken = cancellationToken;
try
{
await ImportExportScheduler(cancellationToken);
}
catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested)
{
Log.Information(ex, "Hosted ImportExportActive shut down normally.");
}
catch (Exception ex)
{
Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportActive");
}
Log.Information("Hosted ImportExportActive is stopping.");
}
private async Task ImportExportScheduler(CancellationToken cancellationToken)
{
do
{
if (DateTime.Now > _nextRun)
{
using (var scope = _scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<IScheduleService>().RunImportExportActive(_cancellationToken);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, cancellationToken); //5 seconds delay
} while (!cancellationToken.IsCancellationRequested);
}
}
}
@@ -0,0 +1,72 @@
using NCrontab;
using ReserveManagementSystemApi.Services;
namespace ReserveManagementSystemApi.HostedService
{
public class HostedImportExportClosed : BackgroundService
{
private readonly IConfiguration _config;
private readonly IServiceScopeFactory _scopeFactory;
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public const string DefaultCronPattern = "15 19 * * 0-6";
private CancellationToken _cancellationToken;
public HostedImportExportClosed(IConfiguration config, IServiceScopeFactory scopeFactory)
{
_config = config;
_scopeFactory = scopeFactory;
try
{
_schedule = CrontabSchedule.Parse(_config.GetSection("HostedServices").GetSection("ImportExportClosedCronSchedule").Value);
}
catch (Exception ex)
{
Log.Error(ex, "Failure parsing crontab in appsettings.");
_schedule = CrontabSchedule.Parse(DefaultCronPattern);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Log.Information("Hosted ImportExportClosed is starting.");
Log.Information($"The import export closed job will run at {_nextRun:M/d/yyyy HH:mm:ss tt}");
_cancellationToken = cancellationToken;
try
{
await ImportExportScheduler(cancellationToken);
}
catch (TaskCanceledException ex) when (_cancellationToken.IsCancellationRequested)
{
Log.Information(ex, "Hosted ImportExportClosed shut down normally.");
}
catch (Exception ex)
{
Log.Error(ex, "Error trying to startup main thread for Hosted ImportExportClosed");
}
Log.Information("Hosted ImportExportClosed is stopping.");
}
private async Task ImportExportScheduler(CancellationToken cancellationToken)
{
do
{
if (DateTime.Now > _nextRun)
{
using (var scope = _scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<IScheduleService>().RunImportExportClosed(_cancellationToken);
}
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, cancellationToken); //5 seconds delay
} while (!cancellationToken.IsCancellationRequested);
}
}
}
@@ -0,0 +1,180 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Mappers
{
public static class ExportMapper
{
//used for Chargeback transactions
private const string GeneralLedgerReceipt = "GLR";
private const string DealerInterestRebate = "DRIR";
private const string ChargeBkDesc = "ChrgBk";
private const string ChargeBkAdjDesc = "ChrgBk Adj";
private const string GeneralLedgerCategory = "NOTE";
private const string GeneralLedgerType = "BAL";
//used for Reserve transactions
private const string DealerExpense = "DLRE";
private const string DealerIncome = "DLRI";
private const string ReserveAdjDesc = "Adjustment";
//used for all transaction types
private const string UpfrontCategory = "UPFR";
private const string DealerType = "DLR";
private const string ElectronicFundType = "EL";
private const string NoHoldClearCategory = "IMED";
private const string FundTypeDetail = "INTR";
public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForReserve(Loan loan)
{
return new GroupedMonetaryTransactionRequest
{
MonetaryTransactionRequests = BuildReserveTransactions(loan),
ParentRequestNumber = loan.LoanNumber
};
}
private static List<MonetaryTransactionRequest> BuildReserveTransactions(Loan loan)
{
var listTransaction = new List<MonetaryTransactionRequest>();
var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0;
var reserveRounded = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(reserveRounded, 2, MidpointRounding.AwayFromZero);
var transactionDescription = isAdjustment ? ReserveAdjDesc : null;
var loanAmount = (double)amount * -1;
listTransaction.Add(BuildParentTransaction(loanAmount, loan.LoanNumber, DealerExpense, transactionDescription));
var dealerAmount = (double)amount;
listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerIncome, transactionDescription));
return listTransaction;
}
public static GroupedMonetaryTransactionRequest MapLoanToIndirectLoanForChargeback(Loan loan, MinorAccountType accountType)
{
var request = new GroupedMonetaryTransactionRequest();
if (accountType != null) {
request.MonetaryTransactionRequests = BuildChargebackTransactions(loan, accountType.GeneralLedgerAccountNumber);
request.ParentRequestNumber = loan.LoanNumber;
}
return request;
}
private static List<MonetaryTransactionRequest> BuildChargebackTransactions(Loan loan, string accountNumber)
{
var listTransaction = new List<MonetaryTransactionRequest>();
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
var isAdjustment = loan.Adjustment != null && loan.Adjustment != 0;
var chargeBackRounded = Math.Round((decimal)loan.ChargeBack, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var amount = isAdjustment ? Math.Round((decimal)loan.Adjustment, 2, MidpointRounding.AwayFromZero) : Math.Round(chargeBackRounded, 2, MidpointRounding.AwayFromZero);
var transactionDescription = isAdjustment ? $"{ChargeBkAdjDesc}-{loan.LoanNumber}" : $"{ChargeBkDesc}-{loan.LoanNumber}";
var loanAmount = (double)amount;
listTransaction.Add(BuildParentTransaction(loanAmount, accountNumber, GeneralLedgerReceipt, transactionDescription));
var dealerAmount = (double)amount * -1;
listTransaction.Add(BuildDealerTransaction(dealerAmount, loan.Dealer.DealerAccountNumber, DealerInterestRebate, transactionDescription));
return listTransaction;
}
private static MonetaryTransactionRequest BuildParentTransaction(double amount, string accountNumber, string transactionType, string transactionDescription)
{
//if a reserve the parent transaction contains the loan number, if chargeback the account is the ledger account for minor account type
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
_balanceTypeList.Add(BuildBalanceType(amount, transactionType));
_fundList.Add(BuildFund(amount));
_fundBalanceTypeList.Add(BuildFundBalanceType(amount));
var transaction = new MonetaryTransactionRequest()
{
AccountNumber = (long)System.Convert.ToUInt64(accountNumber),
TransactionAmount = (transactionType != GeneralLedgerReceipt) ? amount : (double?)null,
RequestNumber = "1",
ParentRequestNumber = null,
TransactionTypeCode = transactionType,
Description = transactionDescription,
BalanceTypes = _balanceTypeList,
Funds = _fundList,
FundBalanceTypes = (transactionType == GeneralLedgerReceipt) ? _fundBalanceTypeList : null,
};
return transaction;
}
private static MonetaryTransactionRequest BuildDealerTransaction(double amount, string dealerAccount, string transactionType, string transactionDescription)
{
//for second request take the offset amount value of parent request amount
_balanceTypeList = new List<BalanceType1>();
_fundList = new List<Fund>();
_fundBalanceTypeList = new List<FundBalanceType>();
_balanceTypeList.Add(BuildBalanceType(amount, transactionType));
_fundList.Add(BuildFund(amount));
_fundBalanceTypeList.Add(BuildFundBalanceType(amount));
var transaction = new MonetaryTransactionRequest()
{
AccountNumber = (string.IsNullOrEmpty(dealerAccount) == true) ? 0 : (long)System.Convert.ToUInt64(dealerAccount),
TransactionAmount = amount,
RequestNumber = "2",
ParentRequestNumber = "1",
TransactionTypeCode = transactionType,
Description = transactionDescription,
BalanceTypes = _balanceTypeList,
Funds = _fundList,
};
return transaction;
}
private static List<FundBalanceType> _fundBalanceTypeList;
private static FundBalanceType BuildFundBalanceType(double amount)
{
return new FundBalanceType
{
FundBalanceAmount = amount,
FundTypeCode = ElectronicFundType,
FundTypeDetailCode = NoHoldClearCategory
};
}
private static List<BalanceType1> _balanceTypeList;
private static BalanceType1 BuildBalanceType(double amount, string transactionType)
{
return new BalanceType1
{
BalanceTypeAmount = amount,
BalanceCategoryCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerCategory : UpfrontCategory,
BalanceTypeCode = (transactionType == GeneralLedgerReceipt) ? GeneralLedgerType : DealerType,
};
}
private static List<Fund> _fundList;
private static Fund BuildFund(double amount)
{
return new Fund
{
ClearingCategoryCode = NoHoldClearCategory,
FundAmount = amount,
FundTypeCode = ElectronicFundType,
FundTypeDetailCode = FundTypeDetail,
NumberOfItems = 1,
};
}
}
}
@@ -0,0 +1,43 @@
using CsvHelper.Configuration.Attributes;
using System;
using System.Text.Json.Serialization;
namespace ReserveManagementSystemApi.Models
{
public class ImportBoatLoan
{
[Index(0)]
public string LoanNumber { get; set; }
[JsonIgnore]
[Index(1)]
public string DealerAccountNumber { get; set; }
[JsonIgnore]
[Index(2)]
public decimal Amount { get; set; }
[JsonIgnore]
[Index(3)]
public string IsChargeback { get; set; }
[JsonIgnore]
[Index(4)]
public string IsAdjustment { get; set; }
[JsonIgnore]
[Index(5)]
[Optional]
public string AccountType { get; set; }
[Index(6)]
[Optional]
public string ErrorMessage { get; set; }
[Index(7)]
[Optional]
public bool? WasSuccessful { get; set; }
}
}
@@ -0,0 +1,113 @@
using CsvHelper.Configuration.Attributes;
using System;
namespace ReserveManagementSystemApi.Models
{
public class ImportNewLoan
{
[Index(0)]
public string DealerOrganizationNumber { get; set; }
[Index(1)]
public string MemberNumber { get; set; }
[Index(2)]
public string LoanNumber { get; set; }
[Index(3)]
public string LastName { get; set; }
[Index(4)]
public string FirstAndMiddleName { get; set; }
[Index(5)]
public DateTime LoanDate { get; set; }
[Index(6)]
public DateTime PostedDate { get; set; }
[Index(7)]
public DateTime FirstPmtDate { get; set; }
[Index(8)]
public DateTime FinalPmtDate { get; set; }
[Index(9)]
public decimal LoanAmount { get; set; }
[Index(10)]
public decimal BuyRate { get; set; }
[Index(11)]
public decimal ContractRate { get; set; }
[Index(12)]
public decimal MonthlyPmt { get; set; }
[Index(13)]
public string LoanType { get; set; }
[Index(14)]
public string InsuranceIndicator { get; set; }
[Index(15)]
public string NewMember { get; set; }
[Index(16)]
[Optional]
public string OriginalTermInDays { get; set; }
[Index(17)]
[Optional]
public string VINNumber { get; set; }
[Index(18)]
[Optional]
public string Make { get; set; }
[Index(19)]
[Optional]
public string Model { get; set; }
[Index(20)]
[Optional]
public string Year { get; set; }
[Index(21)]
[Optional]
public string Mileage { get; set; }
[Index(22)]
[Optional]
public string Color { get; set; }
[Index(23)]
[Optional]
public string ApplicationId { get; set; }
[Index(24)]
[Optional]
public string Warranty { get; set; }
[Index(25)]
[Optional]
public string Gap { get; set; }
[Index(26)]
[Optional]
public string CreditLife { get; set; }
[Index(27)]
[Optional]
public string CreditDisability { get; set; }
[Index(28)]
[Optional]
public string Mbp { get; set; }
[Index(29)]
[Optional]
public string AccountType { get; set; }
}
}
@@ -0,0 +1,249 @@
using ReserveManagementSystemApi.Services;
using ReserveManagementSystemApi.HostedService;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemApi.Configurations;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemLib.Persistence;
using ReserveManagementSystemLib.Repositories;
using MassTransit;
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json;
using LCU.Library.Internal.LcuHttpClient;
using LCU.Library.Internal.ApiSecurity;
using LCU.Library.Internal.Logging.Models;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.Configuration;
using LCU.Library.Internal.LcuMassTransit;
using LCU.Library.Internal.LcuMassTransitModels;
using ReserveManagementSystemApi.Consumers;
using ReserveManagementSystemApi.Services.QueueServices;
using LCU.Library.Internal.Status;
namespace ReserveManagementSystemApi
{
public class Program
{
public static string MyAllowSpecificOrigins { get; private set; }
static void Main(string[] args)
{
MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
WebApplicationBuilder builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
}
public static WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("/app/config/environment-appsettings/appsettings.json", true, true);
builder.Configuration.AddJsonFile("/app/config/workload-appsettings/appsettings.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-seq-api-key.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-rms-database-connection.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-username.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-connection-configuration-queue-password.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/reserve-management-system-api-security-configuration-internal-api-key-set.json", true, true);
builder.Host.UseSerilog();
return builder;
}
public static void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddLCUHttpServices();
var apiConfiguration = configuration.Bind<ApiConfiguration>(services, "Primary");
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins(apiConfiguration.CorsAllowedOrigins).AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(configuration.GetSection("SqlConnectionConfiguration").GetSection("ConnectionString").Value);
});
var connectionConfiguration = MassTransitConfigurationSetUp.AddConnectionConfiguration(ref services, configuration);
Log.Debug(JsonConvert.SerializeObject(connectionConfiguration));
var receiveEndpointConfigurations = new ReceiveEndpointConfigurations()
{
RmsActiveLoanReceiveEndpoint = MassTransitConfigurationSetUp.GetReceiveEndpointConfiguration(configuration, "RmsActiveLoanReceiveEndpoint")
};
services.AddSingleton(receiveEndpointConfigurations);
Log.Debug(JsonConvert.SerializeObject(receiveEndpointConfigurations));
AddMassTransitServices(ref services, connectionConfiguration, receiveEndpointConfigurations, apiConfiguration);
AddQueueServices(services);
var loggerConfiguration = new LoggingConfiguration(apiConfiguration.AppName,
apiConfiguration.SeqApiKey,
apiConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
var rmsApiConfiguration = configuration.Bind<RmsApiConfiguration>(services, "RmsApiConfiguration");
var coreApiConfiguration = configuration.Bind<CoreApiConfiguration>(services, "CoreApiConfiguration");
services.AddApiKeySecurity(configuration);
services.AddLCUStatusServices();
//Services
services.AddScoped<IStatusService, StatusService>();
services.AddScoped<ILoanCalculationService, LoanCalculationService>();
services.AddScoped<ILoanService, LoanService>();
services.AddScoped<IImportService, ImportService>();
services.AddScoped<IImportReadService, ImportReadService>();
services.AddScoped<IAuditHistoryService, AuditHistoryService>();
services.AddScoped<IDealerService, DealerService>();
services.AddScoped<IExportService, ExportService>();
services.AddScoped<IVehicleService, VehicleService>();
services.AddScoped<IScheduleService, ScheduleService>();
services.AddScoped<IRmsActiveLoanService, RmsActiveLoanService>();
//Service Clients
services.AddScoped<ICoreApiServiceClient, CoreApiServiceClient>();
//Repositories
services.AddScoped<ILoanRepository, LoanRepository>();
services.AddScoped<ILoanTypeRepository, LoanTypeRepository>();
services.AddScoped<IImportRepository, ImportRepository>();
services.AddScoped<IAuditHistoryRepository, AuditHistoryRepository>();
services.AddScoped<IDealerRepository, DealerRepository>();
services.AddScoped<IVehicleRepository, VehicleRepository>();
services.AddScoped<IMinorAccountTypeRepository, MinorAccountTypeRepository>();
//HostedServices
if (apiConfiguration.FeatureFlags.EnableImportExportActive)
{
services.AddHostedService<HostedImportExportActive>();
}
if (apiConfiguration.FeatureFlags.EnableImportExportClosed)
{
services.AddHostedService<HostedImportExportClosed>();
}
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = $"{apiConfiguration.AppName}",
Version = "v1",
Contact = new OpenApiContact { Name = "SpecFlow Tests", Url = new Uri("/Specs/index.html", UriKind.Relative) }
});
});
}
public static void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
app.UseSwagger(c =>
{
c.PreSerializeFilters.Add((swaggerDoc, request) =>
{
const string prefixHeader = "X-Forwarded-Prefix";
if (!request.Headers.ContainsKey(prefixHeader)) return;
var serverUrl = request.Headers[prefixHeader];
swaggerDoc.Servers = new List<OpenApiServer>()
{
new() { Description = serverUrl, Url = serverUrl }
};
});
});
app.UseSwaggerUI(c =>
{
c.RoutePrefix = "swagger";
c.SwaggerEndpoint("v1/swagger.json", "ReserveManagementSystemApi v1");
});
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
var specsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Specs");
if (Directory.Exists(specsFolder))
{
app.UseFileServer(new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Specs")),
RequestPath = new PathString("/Specs"),
EnableDefaultFiles = true
});
}
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();
}
public static void AddMassTransitServices(ref IServiceCollection services,
ConnectionConfiguration connectionConfiguration,
ReceiveEndpointConfigurations receiveEndpointConfigurations,
ApiConfiguration apiConfiguration)
{
services.AddMassTransit(x =>
{
x.AddConsumer<RmsActiveImportConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(connectionConfiguration.HostUrl, "/", host =>
{
host.Username(connectionConfiguration.UserName);
host.Password(connectionConfiguration.Password);
});
cfg.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
if (apiConfiguration.FeatureFlags.RunRmsActiveLoanConsumer)
{
cfg.ReceiveEndpoint(receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.ReceiveEndpoint, e =>
{
e.ConfigureConsumer<RmsActiveImportConsumer>(context);
e.SetQuorumQueue();
e.PrefetchCount = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint.PrefetchLimit;
e.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(420));
});
}
});
});
services.AddMassTransitHostedService();
}
public static void AddQueueServices(IServiceCollection services)
{
services.AddScoped<IQueueRmsActiveLoanService, QueueRmsActiveLoanService>();
}
}
}
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.1.1</Version>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<Description></Description>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="LCU.Api.Internal.MaidBotLib" Version="1.0.2" />
<PackageReference Include="LCU.Library.Internal.ApiSecurity" Version="1.0.2" />
<PackageReference Include="LCU.Library.Internal.Configuration" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.CoreApiWrapperModel" Version="2.3.0" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuHttpClient" Version="1.9.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransit" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.LcuMassTransitModels" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="1.1.0" />
<PackageReference Include="LCU.Library.Internal.Status" Version="1.0.1" />
<PackageReference Include="LCU.Library.Internal.StatusModels" Version="1.0.0" />
<PackageReference Include="LCU.Shared.Common.NotificationApiLib" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.18" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.NETCore.App" Version="2.2.8" />
<PackageReference Include="NCrontab.Signed" Version="3.3.2" />
<PackageReference Include="Pickles.CommandLine" Version="2.21.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Specs\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,51 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31025.194
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi", "ReserveManagementSystemApi.csproj", "{954C8B67-1471-4FB2-81BD-713BEE5E679E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemLib", "..\ReserveManagementSystemLib\ReserveManagementSystemLib.csproj", "{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}"
EndProject
Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "ReserveManagementSystemApi.Sql", "..\ReserveManagementSystemApi.Sql\ReserveManagementSystemApi.Sql.sqlproj", "{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReserveManagementSystemApi.Tests", "..\ReserveManagementSystemApi.Tests\ReserveManagementSystemApi.Tests.csproj", "{CFE504B7-58D7-444B-A4B8-8A219400BA42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReserveManagementSystemApi.Test", "..\ReserveManagementSystemApi.Test\ReserveManagementSystemApi.Test.csproj", "{5BC5FBBC-2B72-4055-95E0-41192F581784}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{954C8B67-1471-4FB2-81BD-713BEE5E679E}.Release|Any CPU.Build.0 = Release|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA77A7A0-7D17-4BD8-80A4-B9FFB3908C82}.Release|Any CPU.Build.0 = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Build.0 = Release|Any CPU
{5A41C8DC-6F4D-4FD2-B03D-D0B2D8B363DF}.Release|Any CPU.Deploy.0 = Release|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFE504B7-58D7-444B-A4B8-8A219400BA42}.Release|Any CPU.Build.0 = Release|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BC5FBBC-2B72-4055-95E0-41192F581784}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {27D5180A-D1E5-4786-9F4D-99C4652488F4}
EndGlobalSection
EndGlobal
@@ -0,0 +1,120 @@
using ReserveManagementSystemLib.Configuration;
using LCU.Library.Internal.ApiSecurity.Models;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.LcuHttpClient.HttpRequest;
using LCU.Library.Internal.ApiSecurity.Enums;
using LCU.Library.Internal.LcuHttpClient.Utilities;
namespace ReserveManagementSystemApi.ServiceClients
{
public class CoreApiServiceClient : ICoreApiServiceClient
{
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
private readonly CoreApiConfiguration _coreApiWrapperConfiguration;
private readonly IPostRequest _postRequest;
private readonly IGetRequest _getRequest;
private readonly IPutRequest _putRequest;
private const string ClientKeyValue = "ReserveManagementSystem";
public CoreApiServiceClient(
ApiSecurityConfiguration apiSecurityConfiguration,
CoreApiConfiguration coreApiWrapperConfiguration,
IGetRequest getRequest,
IPostRequest postRequest,
IPutRequest putRequest
)
{
_apiSecurityConfiguration = apiSecurityConfiguration;
_coreApiWrapperConfiguration = coreApiWrapperConfiguration;
_getRequest = getRequest;
_postRequest = postRequest;
_putRequest = putRequest;
}
public async Task<List<MonetaryTransactionResponse>> PostMonetaryTransactions(List<GroupedMonetaryTransactionRequest> loanReserves)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PostMonetaryTransactions}";
var uri = new Uri(path);
var response = await _postRequest.Async<List<GroupedMonetaryTransactionRequest>, List<MonetaryTransactionResponse>>(uri, loanReserves,
HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)),
0, Convert.ToInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error in posting MonetaryTransactions.");
throw;
}
}
public async Task<List<UpdateDealerOriginalBalanceResponse>> PutDealerOriginalBalance(string accountNumber)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.PutDealerOriginalBalance}";
var uri = new Uri(path);
var response = await _putRequest.Async<string, List<UpdateDealerOriginalBalanceResponse>>(uri, accountNumber,
HttpUtilities.GetApiKeyHeaders(ClientKeyValue, _apiSecurityConfiguration.ExtractApiKey(ClientKeyValue, ApiKeySetName.InternalKeySet)),
0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error updating Dealer Original Balance");
throw;
}
}
public async Task<List<IndirectLoanDetail>> GetIndirectLoansByPostedDate(string afterPostDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetIndirectLoansByPostedDate}{afterPostDate}";
var response = await _getRequest.AsyncWithNewton<List<IndirectLoanDetail>>(path, null, 0, Convert.ToUInt16(_coreApiWrapperConfiguration.ApiTimeOutInSeconds));
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Indirect Loans By Posted Date");
return null;
}
}
public async Task<List<ClosedIndirectLoan>> GetClosedLoansByPostedDate(string afterPostDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedLoansByPostedDate}{afterPostDate}";
var response = await _getRequest.AsyncWithNewton<List<ClosedIndirectLoan>>(path);
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Closed Loans By Posted Date");
}
return null;
}
public async Task<List<IndirectLoanDetail>> GetClosedLoansWithDetailByClosedDate(string afterClosedDate)
{
try
{
var path = $"{_coreApiWrapperConfiguration.BaseUrl}{_coreApiWrapperConfiguration.GetClosedIndirectLoansWithDetailByPostedDate}{afterClosedDate}";
var response = await _getRequest.AsyncWithNewton<List<IndirectLoanDetail>>(path);
return response;
}
catch (Exception ex)
{
Log.Error(ex, "Error getting Closed Loans With Detail");
}
return null;
}
}
}
@@ -0,0 +1,19 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
namespace ReserveManagementSystemApi.ServiceClients
{
public interface ICoreApiServiceClient
{
Task<List<MonetaryTransactionResponse>> PostMonetaryTransactions(List<GroupedMonetaryTransactionRequest> loans);
Task<List<IndirectLoanDetail>> GetIndirectLoansByPostedDate(string afterPostDate);
Task<List<ClosedIndirectLoan>> GetClosedLoansByPostedDate(string afterPostDate);
Task<List<IndirectLoanDetail>> GetClosedLoansWithDetailByClosedDate(string afterClosedDate);
Task<List<UpdateDealerOriginalBalanceResponse>> PutDealerOriginalBalance(string accountNumber);
}
}
@@ -0,0 +1,146 @@
using Newtonsoft.Json;
using ReserveManagementSystemApi.Enums;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class AuditHistoryService : IAuditHistoryService
{
private readonly IAuditHistoryRepository _auditHistoryRepository;
const string SYSTEM_MODIFIED_BY = "RMS";
public AuditHistoryService(IAuditHistoryRepository auditHistoryRepository)
{
_auditHistoryRepository = auditHistoryRepository;
}
public async Task<IEnumerable<AuditHistory>> GetAllAsync()
{
try
{
return await _auditHistoryRepository.GetAll().OrderByDescending(o => o.ModifiedDate).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving audit histories.");
throw;
}
}
public async Task<AuditHistory> FindByIdAsync(int id)
{
AuditHistory history;
try
{
history = await _auditHistoryRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving history with id {id}.");
throw;
}
if (history == null)
{
throw new Exception($"Audit history with id {id} not found.");
}
return history;
}
public async Task<AuditHistory> AddHistoryAsync(AuditHistory history)
{
return await _auditHistoryRepository.AddHistoryAsync(history);
}
public async Task<int> AddAuditHistoriesForNonexistingLoansAsync(List<Loan> loans)
{
var currentDate = DateTime.Now;
var histories = loans.Select(loan => new AuditHistory
{
LoanId = null,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.Error.ToString(),
AuditDescription = "Loan in closed file not found",
OldValue = null,
NewValue = null
}).ToList();
return await _auditHistoryRepository.AddHistoriesAsync(histories);
}
public AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan)
{
var currentDate = DateTime.Now;
return new AuditHistory
{
LoanId = null,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.Error.ToString(),
AuditDescription = "Loan in closed import not found",
OldValue = null,
NewValue = null
};
}
public AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan)
{
var currentDate = DateTime.Now;
return new AuditHistory
{
LoanId = loan.LoanId,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = currentDate,
ActivityType = ActivityType.NewLoanUpdate.ToString(),
AuditDescription = "Loan found in table, record updated",
OldValue = JsonConvert.SerializeObject(originalLoan),
NewValue = JsonConvert.SerializeObject(loan)
};
}
public AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description)
{
return new AuditHistory
{
LoanId = loan.LoanId,
MemberNumber = loan.MemberNumber,
LoanNumber = loan.LoanNumber,
ModifiedBy = SYSTEM_MODIFIED_BY,
ModifiedDate = DateTime.Now,
ActivityType = ActivityType.LoanStatusChange.ToString(),
OldValue = oldStatus,
NewValue = loan.LoanStatus,
AuditDescription = description
};
}
public async Task<int> AddHistoriesAsync(List<AuditHistory> histories)
{
return await _auditHistoryRepository.AddHistoriesAsync(histories);
}
public async Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description)
{
var auditHist = new AuditHistory
{
MemberNumber = memberNumber,
LoanNumber = loanNumber,
ActivityType = ActivityType.Error.ToString(),
ModifiedDate = DateTime.Now,
AuditDescription = description,
ModifiedBy = SYSTEM_MODIFIED_BY
};
await _auditHistoryRepository.AddHistoryAsync(auditHist);
}
}
}
@@ -0,0 +1,100 @@
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class DealerService: IDealerService
{
private readonly IDealerRepository _dealerRepository;
public DealerService(IDealerRepository dealerRepository)
{
_dealerRepository = dealerRepository;
}
public async Task<IEnumerable<Dealer>> Search(string dealerSearchText)
{
try
{
return await _dealerRepository.GetAll().Where(d => d.DealerCode.Contains(dealerSearchText) || d.Name.Contains(dealerSearchText)).OrderBy(x => x.DealerCode).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving dealers.");
throw;
}
}
public async Task<Dealer> GetDealerByCodeAsync(string dealerCode)
{
try
{
return await _dealerRepository.FindByCodeAsync(dealerCode);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving dealer by code.");
throw;
}
}
public async Task<IEnumerable<Dealer>> GetAllDealersAsync()
{
return await _dealerRepository.GetAll().ToListAsync();
}
public async Task UpdateDealersAsync(List<Dealer> dealers)
{
try
{
var allDealers = await GetAllDealersAsync();
var dealersToAdd = new List<Dealer>();
var dealersToUpdate = new List<Dealer>();
dealers.ForEach(d =>
{
var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == d.DealerOrganizationNumber);
if (currentDealer != null && !currentDealer.Equals(d))
{
currentDealer.Name = d.Name ?? currentDealer.Name;
currentDealer.DealerCode = d.DealerCode?.ToUpper() ?? currentDealer.DealerCode;
currentDealer.DealerAccountNumber = d.DealerAccountNumber ?? currentDealer.DealerAccountNumber;
currentDealer.DealerOrganizationNumber = d.DealerOrganizationNumber ?? currentDealer.DealerOrganizationNumber;
dealersToUpdate.Add(currentDealer);
}
else if (currentDealer == null)
{
//write the new dealer record to set Action required to true if invalid
dealersToAdd.Add(d);
}
});
if (dealersToUpdate.Any())
{
await _dealerRepository.UpdateDealersAsync(dealersToUpdate);
}
if (dealersToAdd.Any())
{
await _dealerRepository.AddDealersAsync(dealersToAdd);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure updating dealers.");
throw;
}
}
public async Task UpdateDealersAsync(List<Loan> loans)
{
var dealers = loans.Select(l => new Dealer
{
DealerCode = l.Dealer.DealerCode,
Name = l.Dealer.Name,
DealerOrganizationNumber = l.DealerOrganizationNumber,
DealerAccountNumber = l.Dealer.DealerAccountNumber
})
.GroupBy(d => d.DealerOrganizationNumber).Select(g => g.First()).ToList();
await UpdateDealersAsync(dealers);
}
}
}
@@ -0,0 +1,275 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.CoreRequests;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.Transaction;
using ReserveManagementSystemApi.Mappers;
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
using ReserveManagementSystemLib.Configuration;
namespace ReserveManagementSystemApi.Services
{
public class ExportService: IExportService
{
private readonly ILoanRepository _loanRepository;
private readonly ICoreApiServiceClient _coreApiServiceClient;
private readonly IMinorAccountTypeRepository _minorAccountTypeRepository;
private readonly RmsApiConfiguration _rmsApiConfig;
public ExportService(ILoanRepository loanRepository, ICoreApiServiceClient coreApiServiceClient, IMinorAccountTypeRepository minorAccountTypeRepository, RmsApiConfiguration rmsApiConfig)
{
_loanRepository = loanRepository;
_coreApiServiceClient = coreApiServiceClient;
_minorAccountTypeRepository = minorAccountTypeRepository;
_rmsApiConfig = rmsApiConfig;
}
public async Task<int> ExportReservesAsync(DateTime currentDate)
{
var numberOfLoansUpdated = 0;
try
{
var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedReserveDate == null).ToListAsync();
var groupedLoanRequests = loansToUpdate.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList();
var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests);
if (exportResult != null || exportResult.Any())
{
var loansExported = new List<Loan>();
loansToUpdate.ForEach(loan =>
{
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == loan.LoanNumber).FirstOrDefault();
if (validLoan != null && validLoan.WasSuccessful)
{
loan.ExportedReserveDate = currentDate;
numberOfLoansUpdated += 1;
loan.Adjustment = 0;
loansExported.Add(loan); //update successful loan export with exported date
}
else
{
Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}");
}
});
await _loanRepository.UpdateAsync(loansExported);
Log.Information($"Exported {numberOfLoansUpdated} reserves");
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting loan reserves.");
throw;
}
return numberOfLoansUpdated;
}
public async Task<int> ExportChargebacksAsync(DateTime currentDate)
{
var numberOfLoansUpdated = 0;
try
{
var loansToUpdate = await _loanRepository.GetAll().Where(l => l.ExportedChargebackDate == null && l.PaidLoanDate != null).ToListAsync();
var groupedLoanRequests = new List<GroupedMonetaryTransactionRequest>();
loansToUpdate.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType));
}
);
var exportResult = await SendBatchListMonetaryTransactionRequests(groupedLoanRequests);
if (exportResult != null || exportResult.Any())
{
var loansExported = new List<Loan>();
loansToUpdate.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == accountType.GeneralLedgerAccountNumber).FirstOrDefault();
if (validLoan != null && validLoan.WasSuccessful)
{
loan.ExportedChargebackDate = currentDate;
numberOfLoansUpdated += 1;
loan.Adjustment = 0;
loansExported.Add(loan); //update successful loan export with exported date
}
else
{
Log.Error($"Failure to export loan reserve account number: {loan.LoanNumber}");
}
});
await _loanRepository.UpdateAsync(loansExported);
Log.Information($"Exported {numberOfLoansUpdated} chargebacks");
}
else
{
Log.Error("Failure exporting loan chargebacks.");
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting loan chargebacks.");
throw;
}
return numberOfLoansUpdated;
}
public async Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans)
{
var loansExported = new List<ImportBoatLoan>();
try
{
var reserveLoans = loans.Where(l => l.ReserveAmount != 0);
if (reserveLoans != null || reserveLoans.Any())
{
var groupedReserveRequests = reserveLoans.Select(ExportMapper.MapLoanToIndirectLoanForReserve).ToList();
var exportReserveResult = await SendBatchListMonetaryTransactionRequests(groupedReserveRequests);
loansExported.AddRange(MapBoatExportResults(exportReserveResult, reserveLoans.ToList()));
}
var chargebackLoans = loans.Where(l => l.ChargeBack != 0).ToList();
if (chargebackLoans != null || chargebackLoans.Any())
{
var groupedLoanRequests = new List<GroupedMonetaryTransactionRequest>();
chargebackLoans.ForEach(loan =>
{
var accountType = GetMinorAccountType(loan.AccountType);
groupedLoanRequests.Add(ExportMapper.MapLoanToIndirectLoanForChargeback(loan, accountType));
}
);
var exportResult = await _coreApiServiceClient.PostMonetaryTransactions(groupedLoanRequests);
loansExported.AddRange(MapBoatExportResults(exportResult, chargebackLoans.ToList()));
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure exporting boat loans.");
throw;
}
return loansExported;
}
public async Task<int> MarkReservesAsExportedAsync(List<Loan> loans, DateTime currentDate)
{
try
{
loans.ForEach(l =>
{
l.ExportedReserveDate = currentDate;
});
await _loanRepository.UpdateAsync(loans);
Log.Information($"Marked {loans.Count} loan reserves as exported (without exporting)");
}
catch (Exception ex)
{
Log.Error(ex, "Failure marking loan reserves as exported.");
throw;
}
return loans.Count;
}
public async Task<int> MarkReservesAndChargebacksAsExportedAsync(List<Loan> loans, DateTime currentDate)
{
try
{
loans.ForEach(l =>
{
l.ExportedReserveDate = currentDate;
l.ExportedChargebackDate = currentDate;
});
await _loanRepository.UpdateAsync(loans);
Log.Information($"Marked {loans.Count} loan reserves and chargebacks as exported (without exporting)");
}
catch (Exception ex)
{
Log.Error(ex, "Failure marking loan reserves and chargebacks as exported.");
throw;
}
return loans.Count;
}
private List<ImportBoatLoan> MapBoatExportResults(List<MonetaryTransactionResponse> exportResult, List<Loan> loansToUpdate)
{
var numberOfLoansUpdated = 0;
var loansExported = new List<ImportBoatLoan>();
if (exportResult != null || exportResult.Any())
{
loansToUpdate.ForEach(loan =>
{
var boatLoan = new ImportBoatLoan();
string acccountNumber;
if (loan.ReserveAmount != 0)
{
acccountNumber = loan.LoanNumber;
}
else
{
var accountType = GetMinorAccountType(loan.AccountType);
acccountNumber = accountType.GeneralLedgerAccountNumber;
}
var validLoan = exportResult.Where(e => e.AccountNumber.ToString() == acccountNumber).Any();
var validDealer = exportResult.Where(e => e.AccountNumber.ToString() == loan.Dealer.DealerAccountNumber).Any();
if (validLoan && validDealer)
{
numberOfLoansUpdated += 1;
boatLoan.LoanNumber = loan.LoanNumber;
boatLoan.WasSuccessful = true;
}
else
{
var error = $"Failure to export boat loan account number: {loan.LoanNumber}";
boatLoan.LoanNumber = loan.LoanNumber;
boatLoan.WasSuccessful = false;
boatLoan.ErrorMessage = error;
Log.Error(error);
}
loansExported.Add(boatLoan);
});
Log.Information($"Exported {numberOfLoansUpdated} boat loans");
}
return loansExported;
}
private MinorAccountType GetMinorAccountType(string loanAccountType)
{
var result = _minorAccountTypeRepository.GetMinorAccountType(loanAccountType);
return result;
}
public async Task<List<MonetaryTransactionResponse>> SendBatchListMonetaryTransactionRequests(List<GroupedMonetaryTransactionRequest> groupedLoanRequests)
{
var result = new List<MonetaryTransactionResponse>();
var batchSize = _rmsApiConfig.DefaultBatchSize;
foreach (var batch in BatchHelper.CreateBatch<GroupedMonetaryTransactionRequest>(groupedLoanRequests, batchSize))
{
var response = await _coreApiServiceClient.PostMonetaryTransactions(batch.ToList());
result.AddRange(response);
};
return result;
}
}
}
@@ -0,0 +1,25 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IAuditHistoryService
{
Task<IEnumerable<AuditHistory>> GetAllAsync();
Task<AuditHistory> FindByIdAsync(int id);
Task<AuditHistory> AddHistoryAsync(AuditHistory history);
Task<int> AddAuditHistoriesForNonexistingLoansAsync(List<Loan> loans);
AuditHistory GetExistingLoanUpdateHistoryAsync(Loan loan, Loan originalLoan);
AuditHistory GetAuditHistoryForNonexistingClosedLoan(Loan loan);
AuditHistory GetLoanStatusChangeHistory(Loan loan, string oldStatus, string description);
Task<int> AddHistoriesAsync(List<AuditHistory> histories);
Task AddErrorHistoryAsync(string memberNumber, string loanNumber, string description);
}
}
@@ -0,0 +1,17 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IDealerService
{
Task<IEnumerable<Dealer>> Search(string dealerSearch);
Task<Dealer> GetDealerByCodeAsync(string dealerCode);
Task UpdateDealersAsync(List<Dealer> dealers);
Task UpdateDealersAsync(List<Loan> loans);
Task<IEnumerable<Dealer>> GetAllDealersAsync();
}
}
@@ -0,0 +1,18 @@
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IExportService
{
Task<int> ExportReservesAsync(DateTime currentDate);
Task<int> ExportChargebacksAsync(DateTime currentDate);
Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans);
Task<int> MarkReservesAsExportedAsync(List<Loan> loans, DateTime currentDate);
Task<int> MarkReservesAndChargebacksAsExportedAsync(List<Loan> loans, DateTime currentDate);
}
}
@@ -0,0 +1,28 @@
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IImportReadService
{
Task<Import> ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false);
Task<Import> ImportClosedLoansFromDNA(string newerThanDateTime);
Task<Import> ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport);
Task<Import> ProcessActiveLoanImport(List<IndirectLoanDetail> loanDetails, DateTime currentTime, bool skipExport);
Task<Import> ProcessClosedLoanImport(List<ClosedIndirectLoan> closedLoans, DateTime currentTime, string newerThanDateTime);
Task<Import> ProcessClosedLoansWithUpdates(List<IndirectLoanDetail> dnaLoans, DateTime currentTime, bool skipExport);
Task<Import> ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime);
Task<List<ImportBoatLoan>> ReadBoatLoanImportFile(StreamReader importFile);
Task<Import> ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime);
}
}
@@ -0,0 +1,20 @@
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IImportService
{
Task<IEnumerable<Import>> GetAllAsync();
Task<IEnumerable<Import>> GetRecentAsync(int numberOfImports);
Task<Import> GetByIdAsync(int id);
Task<Import> AddImportAsync(Import import);
Task<Import> UpdateImportAsync(Import import);
Task<Import> GetLastLoanImportByType(ImportType importType);
}
}
@@ -0,0 +1,13 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface ILoanCalculationService
{
LoanReserve CalculateSingleReserve(Loan loan);
LoanChargeback CalculateSingleChargeback(Loan loan);
Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportedReserveDate = null);
}
}
@@ -0,0 +1,31 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface ILoanService
{
Task<IEnumerable<Loan>> GetAllAsync();
Task<PaginatedListResponse> GetFilteredAsync(LoanFilter filter);
Task<Loan> GetByIdAsync(int id);
Task<LoanReserve> GetLoanReserveAsync(LoanReserveInput input);
Task<LoanChargeback> GetLoanChargebackAsync(LoanChargebackInput input);
Task<List<Loan>> SaveLoansAsync(List<Loan> loans);
Task<List<Loan>> SaveWithChargebackAsync(List<Loan> loans);
Task<List<Loan>> SaveWithReserveAndChargebackAsync(List<Loan> loans);
Task<List<string>> SearchMembers(string searchText);
Task<List<string>> SearchLoanNumbers(string searchText);
Task<List<string>> SearchApplicationIds(string searchText);
Task UpdateDealerOriginalBalanceAsync(List<string> accountNumbers);
}
}
@@ -0,0 +1,9 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IRmsActiveLoanService
{
Task HandleRmsActiveImportRequest(RmsActiveImportRequest request);
}
}
@@ -0,0 +1,8 @@
namespace ReserveManagementSystemApi.Services
{
interface IScheduleService
{
Task RunImportExportActive(CancellationToken cancellationToken);
Task RunImportExportClosed(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,7 @@
using LCU.Library.Internal.StatusModels.Models;
namespace ReserveManagementSystemApi.Services;
public interface IStatusService
{
EnvironmentView GetEnviromentView();
}
@@ -0,0 +1,10 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public interface IVehicleService
{
Task<Vehicle> GetVehicleByLoanIdAsync(int? loanId);
}
}
@@ -0,0 +1,402 @@
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemApi.Models;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Helpers;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Configuration;
using ReserveManagementSystemApi.Enums;
using LCU.Api.Internal.MaidBotLib.Models;
using LCU.Library.Internal.CoreApiWrapperModel.CoreModels.IndirectLoan;
namespace ReserveManagementSystemApi.Services
{
public class ImportReadService : IImportReadService
{
private readonly ILoanService _loanService;
private readonly IImportService _importService;
private readonly IDealerService _dealerService;
private readonly IExportService _exportService;
private readonly ICoreApiServiceClient _coreApiServiceClient;
private readonly RmsApiConfiguration _rmsApiConfig;
private ImportHelper _importHelper = new ImportHelper();
public ImportReadService(ILoanService loanService,
IImportService importService,
IDealerService dealerService,
IExportService exportService,
ICoreApiServiceClient coreApiServiceClient,
RmsApiConfiguration rmsApiConfig)
{
_loanService = loanService;
_importService = importService;
_dealerService = dealerService;
_exportService = exportService;
_coreApiServiceClient = coreApiServiceClient;
_rmsApiConfig = rmsApiConfig;
}
public async Task<Import> ImportActiveLoansFromDNA(string newerThanDateTime, bool skipExport = false)
{
if (string.IsNullOrEmpty(newerThanDateTime))
{
Import lastImport = await _importService.GetLastLoanImportByType(ImportType.Active);
newerThanDateTime = GetStartDateForNextImport(lastImport);
}
Log.Information("Get Active Loans By Posted Date is starting.");
var dnaLoans = await _coreApiServiceClient.GetIndirectLoansByPostedDate(newerThanDateTime);
Log.Information($"Get Active Loans By Posted Date has ended. Found {dnaLoans.Count()} active loan records.");
Log.Information("Starting ProcessActiveLoanImport");
var import = await ProcessActiveLoanImport(dnaLoans, DateTime.Now, skipExport);
Log.Information("ProcessActiveLoanImport has ended");
return import;
}
public async Task<Import> ImportClosedLoansFromDNA(string newerThanDateTime = "")
{
if (string.IsNullOrEmpty(newerThanDateTime))
{
var lastImport = await _importService.GetLastLoanImportByType(ImportType.Closed);
newerThanDateTime = GetStartDateForNextImport(lastImport);
}
var dnaLoans = await _coreApiServiceClient.GetClosedLoansByPostedDate(newerThanDateTime);
var import = await ProcessClosedLoanImport(dnaLoans, DateTime.Now, newerThanDateTime);
return import;
}
public async Task<Import> ImportClosedLoansWithUpdatesFromDNA(string newerThanDateTime, bool skipExport)
{
var dnaLoans = await _coreApiServiceClient.GetClosedLoansWithDetailByClosedDate(newerThanDateTime);
var newImport = new Import();
try
{
newImport = await ProcessClosedLoansWithUpdates(dnaLoans, DateTime.Now, skipExport);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import closed loans from DNA.");
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending))
.Error(ex, $"Reserve Management System Failure. " +
$"Error while importing Closed Loans from DNA.");
throw;
}
return newImport;
}
public async Task<Import> ProcessActiveLoanImport(List<IndirectLoanDetail> loanDetails, DateTime currentTime, bool skipExport = false)
{
var newImport = new Import
{
ImportType = ImportType.Active.ToString(),
StartDate = currentTime
};
newImport = await _importService.AddImportAsync(newImport);
try
{
var loans = _importHelper.TransformLoanFromDNA(loanDetails, newImport.ImportId); //Transform import to loan object
await _dealerService.UpdateDealersAsync(loans);
var allDealers = await _dealerService.GetAllDealersAsync();
loans.ForEach(loan =>
{
var currentDealer = allDealers.FirstOrDefault(a => a.DealerOrganizationNumber == loan.DealerOrganizationNumber);
loan.Dealer = currentDealer; // assign the loan the updated dealer then check validation in save method
});
await SaveLoansExportAndImportAsync(newImport, loans, currentTime, skipExport);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import active loans from DNA.");
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.IndirectLending))
.Error(ex, $"Reserve Management System Failure. " +
$"Error while importing Active Loans from DNA.");
throw;
}
return newImport;
}
public async Task<Import> ProcessClosedLoansWithUpdates(List<IndirectLoanDetail> dnaLoans, DateTime currentTime, bool skipExport)
{
var closedImport = new Import
{
ImportType = ImportType.Closed.ToString(),
StartDate = currentTime
};
closedImport = await _importService.AddImportAsync(closedImport);
var loans = _importHelper.TransformLoanFromDNA(dnaLoans, closedImport.ImportId);
await _dealerService.UpdateDealersAsync(loans);
var loansClosed = await _loanService.SaveWithReserveAndChargebackAsync(loans);
if (!skipExport)
{
//prevent converted loans from exporting
DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate);
var convertedLoansMarkExported = loansClosed.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "Closed").ToList();
await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime);
//update dealer original balance with only non-converted loans
await _exportService.ExportReservesAsync(DateTime.Now);
var nonConvertedLoans = loansClosed.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList();
var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers);
await _exportService.ExportChargebacksAsync(DateTime.Now);
}
else
{
await _exportService.MarkReservesAndChargebacksAsExportedAsync(loansClosed, DateTime.Now);
}
closedImport.EndDate = DateTime.Now;
closedImport.RecordCount = loansClosed.Count;
closedImport.RecordsSaved = loansClosed.Count;
closedImport.Success = true;
await _importService.UpdateImportAsync(closedImport);
return closedImport;
}
public async Task<Import> ProcessClosedLoanImport(List<ClosedIndirectLoan> closedLoans, DateTime currentTime, string newerThanDateTime)
{
List<Loan> loansUpdated;
var closedImport = new Import
{
ImportType = ImportType.Closed.ToString(),
StartDate = currentTime
};
closedImport = await _importService.AddImportAsync(closedImport);
try
{
loansUpdated = await CloseLoans(closedLoans, closedImport);
var needDetails = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList();
if (needDetails.Count > 0)
{
await ImportClosedLoansWithUpdatesFromDNA(newerThanDateTime, false);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failure to import closed loans from DNA.");
throw;
}
return closedImport;
}
public async Task<Import> ReadNewLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime)
{
var loansFromImport = new List<ImportNewLoan>();
var newImport = new Import
{
FileName = fileName.Truncate(50),
StartDate = currentTime,
ImportType = ImportType.Active.ToString()
};
newImport = await _importService.AddImportAsync(newImport);
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
TrimOptions = TrimOptions.Trim,
HeaderValidated = null,
MissingFieldFound = null
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ImportNewLoan>().ToList();
}
var dealers = loansFromImport.Select(l => new Dealer
{
DealerOrganizationNumber = l.DealerOrganizationNumber
})
.GroupBy(d => d.DealerOrganizationNumber).First().ToList();
await _dealerService.UpdateDealersAsync(dealers);
var loans = _importHelper.TransformNewLoan(loansFromImport, newImport.ImportId); //Transform import to loan object
await SaveLoansExportAndImportAsync(newImport, loans, currentTime);
}
catch (Exception ex)
{
Log.Error("Failure to read new loan file, message: {0} {1}", ex.Message, ex.StackTrace);
throw;
}
return newImport;
}
public async Task<List<ImportBoatLoan>> ReadBoatLoanImportFile(StreamReader importFile)
{
var loansFromImport = new List<ImportBoatLoan>();
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = false,
TrimOptions = TrimOptions.Trim,
HeaderValidated = null,
MissingFieldFound = null,
BadDataFound = null
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ImportBoatLoan>().ToList();
}
var loans = _importHelper.TransformBoatLoan(loansFromImport);
loansFromImport = await ExportBoatLoansAsync(loans);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to read boat loan file");
throw;
}
return loansFromImport;
}
public async Task<Import> ReadClosedLoanImportFile(StreamReader importFile, string fileName, DateTime currentTime)
{
var loansFromImport = new List<ClosedIndirectLoan>();
List<Loan> loansUpdated;
var closedImport = new Import
{
FileName = fileName.Truncate(50),
StartDate = currentTime,
ImportType = ImportType.Closed.ToString()
};
closedImport = await _importService.AddImportAsync(closedImport);
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
TrimOptions = TrimOptions.Trim,
PrepareHeaderForMatch = args => args.Header.ToLower()
};
try
{
using (var csv = new CsvReader(importFile, config))
{
loansFromImport = csv.GetRecords<ClosedIndirectLoan>().ToList();
}
loansUpdated = await CloseLoans(loansFromImport, closedImport);
Log.Information($"Closed {loansUpdated.Count} loans");
await _exportService.ExportChargebacksAsync(DateTime.Now);
}
catch (Exception ex)
{
Log.Error("Failure to read closed loan file, message: {0} {1}", ex.Message, ex.StackTrace);
throw;
}
return closedImport;
}
private async Task<List<Loan>> CloseLoans(List<ClosedIndirectLoan> loansFromImport, Import closedImport)
{
var loans = _importHelper.TransformClosedLoans(loansFromImport, closedImport.ImportId).ToList();
var loansUpdated = await _loanService.SaveWithChargebackAsync(loans);
var missingActiveLoans = loansUpdated.Where(l => l.LoanStatus == LoanStatus.MissingActiveLoan.ToString()).ToList();
await _exportService.ExportChargebacksAsync(DateTime.Now);
closedImport.EndDate = DateTime.Now;
closedImport.RecordCount = loansFromImport.Count;
closedImport.RecordsSaved = loansUpdated.Count - missingActiveLoans.Count;
closedImport.Success = true;
await _importService.UpdateImportAsync(closedImport);
Log.Information($"Closed {loansUpdated.Count} loans");
return loansUpdated;
}
private async Task<List<ImportBoatLoan>> ExportBoatLoansAsync(List<Loan> loans, bool skipExport = false)
{
var boatResults = new List<ImportBoatLoan>();
if (!skipExport)
{
boatResults = await _exportService.ExportBoatLoansAsync(loans);
var reserveLoans = loans.Where(l => l.ReserveAmount != 0);
var listAccounts = reserveLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccounts);
}
Log.Information($"Imported {loans.Count} boat loans");
return boatResults;
}
private string GetStartDateForNextImport(Import lastImport)
{
if (lastImport?.StartDate != null)
{
return lastImport.StartDate.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
}
return _rmsApiConfig.DefaultImportStartDate;
}
private async Task SaveLoansExportAndImportAsync(Import import, List<Loan> loans, DateTime currentTime, bool skipExport = false)
{
var loansImported = await _loanService.SaveLoansAsync(loans);
if (!skipExport)
{
//prevent converted loans from exporting
DateTime defaultConvertedPostDate = Convert.ToDateTime(_rmsApiConfig.DefaultConvertedLoanPostDate);
var convertedLoansMarkExported = loansImported.Where(loan => loan.PostedDate < defaultConvertedPostDate && loan.LoanStatus == "New").ToList();
await _exportService.MarkReservesAsExportedAsync(convertedLoansMarkExported, currentTime);
//update dealer original balance with only non-converted loans
await _exportService.ExportReservesAsync(currentTime);
var nonConvertedLoans = loansImported.Where(loan => loan.PostedDate > defaultConvertedPostDate).ToList();
var listAccountNumbers = nonConvertedLoans.Select(loan => loan.LoanNumber).ToList();
await _loanService.UpdateDealerOriginalBalanceAsync(listAccountNumbers);
}
else
{
await _exportService.MarkReservesAsExportedAsync(loansImported, currentTime);
}
Log.Information($"Imported {loansImported.Count} active loans");
import.EndDate = DateTime.Now;
import.RecordCount = loansImported.Count;
import.RecordsSaved = loansImported.Count;
import.Success = true;
await _importService.UpdateImportAsync(import);
}
}
}
@@ -0,0 +1,89 @@
using ReserveManagementSystemLib.Enums;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class ImportService: IImportService
{
private readonly IImportRepository _importRepository;
public ImportService(IImportRepository importRepository)
{
_importRepository = importRepository;
}
public async Task<IEnumerable<Import>> GetAllAsync()
{
try
{
return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving imports.");
throw;
}
}
public async Task<Import> GetLastLoanImportByType(ImportType importType)
{
Import import;
try
{
import = await _importRepository.GetLastLoanDnaImportByType(importType).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving last import.");
throw;
}
return import;
}
public async Task<IEnumerable<Import>> GetRecentAsync(int numberOfImports)
{
try
{
return await _importRepository.GetAll().OrderByDescending(imp => imp.StartDate).Take(numberOfImports).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving imports.");
throw;
}
}
public async Task<Import> GetByIdAsync(int id)
{
Import import;
try
{
import = await _importRepository.FindByIdAsync(id);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving import with id {id}.");
throw;
}
if (import == null)
{
throw new Exception($"Import with id {id} not found.");
}
return import;
}
public async Task<Import> AddImportAsync(Import newImport)
{
return await _importRepository.AddImportAsync(newImport);
}
public async Task<Import> UpdateImportAsync(Import import)
{
return await _importRepository.UpdateImportAsync(import);
}
}
}
@@ -0,0 +1,314 @@
using ReserveManagementSystemApi.Helpers;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public class LoanCalculationService : ILoanCalculationService
{
private readonly LoanCalculationHelper _loanCalculationHelper;
private readonly BoatLoanCalculationHelper _boatLoanCalculationHelper;
private CalculationConfig _calculationConfig;
public LoanCalculationService()
{
_loanCalculationHelper = new LoanCalculationHelper();
_boatLoanCalculationHelper = new BoatLoanCalculationHelper();
}
public LoanReserve CalculateSingleReserve(Loan loan)
{
var result = new LoanReserve();
try
{
_calculationConfig = new CalculationConfig(loan);
if (!loan.IsBoatLoan(loan))
{
result = ReserveCalculation(loan);
}
else
{
result = BoatReserveCalculation(loan);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan: {0} {1}", e.Message, e.StackTrace);
result.ReserveTypeMessage = "Exception while calculating reserve";
}
return result;
}
public LoanChargeback CalculateSingleChargeback(Loan loan)
{
var result = new LoanChargeback();
try
{
_calculationConfig = new CalculationConfig(loan);
if (!loan.IsBoatLoan(loan))
{
result = ChargebackCalculation(loan);
}
else
{
result = BoatChargebackCalculation(loan);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Chargeback: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public Loan CalculateLoanReserve(Loan loan, decimal? originalReserve = null, DateTime? originalExportReserveDate = null)
{
try
{
_calculationConfig = new CalculationConfig(loan);
var loanReserve = new LoanReserve();
if (!loan.IsBoatLoan(loan))
{
loanReserve = ReserveCalculation(loan);
}
else
{
loanReserve = BoatReserveCalculation(loan);
}
loan = AssignReserveValues(loan, loanReserve);
if (originalReserve.HasValue)
{
CalculateReserveAdjustment(loan, originalReserve, originalExportReserveDate);
}
}
catch (Exception e)
{
Log.Error("Failure to Calculate Reserve: {0} {1}", e.Message, e.StackTrace);
loan.ReserveMessage = "Exception while calculating reserve";
}
return loan;
}
private static void CalculateReserveAdjustment(Loan loan, decimal? originalReserve, DateTime? originalExportReserveDate)
{
var updatedReserveSaved = Math.Round(loan.ReserveAmount, 4, MidpointRounding.AwayFromZero); //this represents the amount saved to DB
var updatedReserveRounded = Math.Round(updatedReserveSaved, 2, MidpointRounding.AwayFromZero);
var originalReserveRounded = Math.Round(originalReserve.Value, 2, MidpointRounding.AwayFromZero);
if (updatedReserveRounded != originalReserveRounded)
{
loan.Adjustment = (updatedReserveRounded - originalReserveRounded);
loan.ExportedReserveDate = null; //we only want to remove last export date if reserve amount changed
}
else
{
loan.ExportedReserveDate = originalExportReserveDate;
}
}
private LoanReserve ReserveCalculation(Loan loanData)
{
var loanReserve = new LoanReserve();
_calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate%
_calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate%
if (loanData.ContractRate < loanData.BuyRate)
{
//Calculate Buy Down.
return _loanCalculationHelper.CalculateBuyDown(_calculationConfig);
}
else
{
// Calculate Reserve Amount for all other loans.
if ((double)loanData.ContractRate == (double)loanData.BuyRate)
{
loanReserve = _loanCalculationHelper.CalculateNoMarkup(_calculationConfig);
}
else
{
loanReserve = _loanCalculationHelper.CalculateStandardMarkup(_calculationConfig);
}
//Assign any Flat Fees if applicable
return _loanCalculationHelper.AssignFlatFee(loanReserve, _calculationConfig);
}
}
private LoanReserve BoatReserveCalculation(Loan loanData)
{
if (loanData.ContractRate < loanData.BuyRate)
{
_calculationConfig.BuyFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.BuyRate); // Finance charge using BuyRate%
_calculationConfig.ContractFinanceCharge = _loanCalculationHelper.FinanceCharge(_calculationConfig.LoanData().Term, loanData.LoanAmount, loanData.ContractRate); // Finance charge using ContractRate%
return _loanCalculationHelper.CalculateBuyDown(_calculationConfig);
}
else
{
// Calculate Reserve Amount for all other loans.
return _boatLoanCalculationHelper.CalculateStandardMarkup(_calculationConfig);
}
}
private Loan AssignReserveValues(Loan loan, LoanReserve loanReserve)
{
loan.ReserveAmount = loanReserve.ReserveAmount;
loan.ReserveMessage = loanReserve.ReserveTypeMessage;
loan.Term = _calculationConfig.LoanData().Term;
loan.FlatFeePaid = loanReserve.FlatFeePaid;
return loan;
}
private LoanChargeback ChargebackCalculation(Loan loanData)
{
if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN"))
{
return SetBuyDownChargeback(loanData);
}
if (loanData.LoanType?.ChargebackMethod == "S")
{
return CalculateBBLoans(loanData);
}
else
{
return CalculateLoanWithChargebackDate(loanData);
}
}
private LoanChargeback BoatChargebackCalculation(Loan loanData)
{
if (loanData.ReserveAmount < 0 || loanData.ReserveMessage != null && loanData.ReserveMessage.ToUpper().Contains("BUYDOWN"))
{
return SetBuyDownChargeback(loanData);
}
else
{
return CalculateBoatLoanWithChargebackDate(loanData);
}
}
private LoanChargeback CalculateBoatLoanWithChargebackDate(Loan loanData)
{
var numOfChargebackPayments = loanData.Term <= 63 ? 6 : 12; //term is 63 or less chargeback date is 6 monthly payments, otherwise 12
var lastChargebackDate = _boatLoanCalculationHelper.GetLastChargebackDate(loanData.FirstPmtDate, numOfChargebackPayments);
if (loanData.PaidLoanDate > lastChargebackDate)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
return ChargebackWithRemainingMonths(loanData);
}
private LoanChargeback CalculateLoanWithChargebackDate(Loan loanData)
{
var numOfChargebackPayments = loanData.LoanType?.NoChargebackPayments ?? 3;
var numOfChargebackDays = loanData.LoanType?.NoChargebackDays ?? 90;
var lastChargebackDate = _loanCalculationHelper.GetLastChargebackDate(loanData.LoanDate, loanData.FirstPmtDate, numOfChargebackPayments, numOfChargebackDays);
if (loanData.PaidLoanDate > lastChargebackDate)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : "Paid After Chargeback Date",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
return ChargebackWithRemainingMonths(loanData);
}
private LoanChargeback ChargebackWithRemainingMonths(Loan loanData)
{
int remainingMonths = 0;
string message = "Paid On or Before Chargeback Date";
remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData);
return new LoanChargeback
{
//this needs to be the original term formula
ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths),
ChargebackTypeMessage = loanData.ReserveAmount == 0 ? "No Reserve - No Chargeback" : message,
RemainingMonthsAtClose = (byte)remainingMonths
};
}
private LoanChargeback ChargebackWithRemainingMonthsBB(Loan loanData)
{
int remainingMonths = 0;
string message = "Paid After Chargeback Date";
//add chargeback date for BB Loans
var chargeBackDate = loanData.FirstPmtDate.AddMonths(loanData.Term - 1);
if (loanData.PaidLoanDate <= chargeBackDate)
{
remainingMonths = _loanCalculationHelper.GetRemainingMonths(loanData);
if (remainingMonths <= 0)
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = message,
RemainingMonthsAtClose = 0
};
}
else
{
message = "Paid On or Before Chargeback Date";
}
}
return new LoanChargeback
{
ChargebackAmount = _loanCalculationHelper.CalculateChargeback(loanData.ReserveAmount, loanData.Term, remainingMonths),
ChargebackTypeMessage = message,
RemainingMonthsAtClose = (byte)remainingMonths
};
}
private LoanChargeback CalculateBBLoans(Loan loanData)
{
if (loanData.ReserveAmount != 0)
{
return ChargebackWithRemainingMonthsBB(loanData);
}
else
{
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = "No Reserve - No Chargeback",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
}
private LoanChargeback SetBuyDownChargeback(Loan loanData)
{
//Buydown scenarios will not calculate a chargeback and return a message indicating this.
return new LoanChargeback
{
ChargebackAmount = 0M,
ChargebackTypeMessage = "Buy Down Reserve - No Chargeback",
RemainingMonthsAtClose = (byte)_loanCalculationHelper.GetRemainingMonths(loanData)
};
}
}
}
@@ -0,0 +1,448 @@
using System.Linq.Dynamic.Core;
using ReserveManagementSystemApi.Enums;
using ReserveManagementSystemApi.ServiceClients;
using ReserveManagementSystemLib.Helpers;
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class LoanService : ILoanService
{
private readonly ILoanRepository _loanRepository;
private readonly ILoanTypeRepository _loanTypeRepository;
private readonly IAuditHistoryService _auditHistoryService;
private ILoanCalculationService _loanCalculationService;
private const double MonthInDays = 30.4375;
private readonly ICoreApiServiceClient _coreApiServiceClient;
public LoanService(ILoanRepository loanRepository, ILoanTypeRepository loanTypeRepository, IAuditHistoryService auditHistoryService, ICoreApiServiceClient coreApiServiceClient)
{
_loanRepository = loanRepository;
_loanTypeRepository = loanTypeRepository;
_auditHistoryService = auditHistoryService;
_coreApiServiceClient = coreApiServiceClient;
_loanCalculationService = new LoanCalculationService();
}
public async Task<IEnumerable<Loan>> GetAllAsync()
{
try
{
return await _loanRepository.GetAll().OrderByDescending(x => x.ImportId).ThenBy(y => y.MemberNumber).ThenBy(z => z.LoanNumber).ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving loans.");
throw;
}
}
public async Task<Loan> GetByIdAsync(int id)
{
Loan loan;
try
{
loan = await _loanRepository.FindByIdAsync(id);
if (loan != null)
{
loan.ValidationErrors = ValidationHelper.SetValidationErrors(loan);
}
}
catch (Exception ex)
{
Log.Error(ex, $"Failure retrieving loan with id {id}.");
throw;
}
if (loan == null)
{
throw new Exception($"Loan with id {id} not found.");
}
return loan;
}
public async Task<PaginatedListResponse> GetFilteredAsync(LoanFilter filter)
{
try
{
IQueryable<Loan> loans;
PaginatedListResponse pagedListResponse = new PaginatedListResponse();
var query = _loanRepository.GetAll();
var filteredQuery = filter.ApplyFilter(query);
List<string> listOfFieldNames = typeof(Loan).GetProperties().Select(f => f.Name).ToList();
listOfFieldNames.AddRange(typeof(Dealer).GetProperties().Select(f => $"Dealer.{f.Name}").ToList());
listOfFieldNames.AddRange(typeof(Vehicle).GetProperties().Select(f => $"Vehicle.{f.Name}").ToList());
var result = listOfFieldNames.Where(n => n == filter.SortField).FirstOrDefault();
if (!string.IsNullOrEmpty(filter.SortField) && result != null)
{
loans = filteredQuery.OrderBy($"{filter.SortField} {filter.SortDirection}, ImportId desc, MemberNumber, LoanNumber");
}
else
{
loans = filteredQuery.OrderBy($"ImportId desc, MemberNumber, LoanNumber");
}
pagedListResponse = await PaginatedListResponse.CreateListAsync(loans, filter.PageIndex, filter.MaxRecordsPerPage, filter.MaxPagesPerRange);
return pagedListResponse;
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving loans with filter.");
throw;
}
}
public async Task<LoanReserve> GetLoanReserveAsync(LoanReserveInput input)
{
var result = new LoanReserve();
var loan = new Loan
{
LoanDate = input.LoanDate,
FirstPmtDate = input.FirstPmtDate,
LoanAmount = input.LoanAmount,
LoanTypeId = input.LoanType,
BuyRate = input.BuyRate,
ContractRate = input.ContractRate,
Term = (int)Math.Round(Convert.ToDecimal(input.OriginalTermInDays) / (decimal)MonthInDays, 0),
AccountType = input.AccountType
};
loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId);
try
{
_loanCalculationService = new LoanCalculationService();
result = _loanCalculationService.CalculateSingleReserve(loan);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Reserve: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public async Task<LoanChargeback> GetLoanChargebackAsync(LoanChargebackInput input)
{
var result = new LoanChargeback();
var loan = new Loan
{
LoanDate = input.LoanDate,
FirstPmtDate = input.FirstPmtDate,
FinalPmtDate = input.FinalPmtDate,
LoanTypeId = input.LoanType,
ReserveAmount = input.ReserveAmount,
PaidLoanDate = input.PaidLoanDate,
Term = input.Term,
AccountType = input.AccountType
};
loan.LoanType = await _loanTypeRepository.GetLoanTypeAsync(loan.LoanTypeId);
try
{
result = _loanCalculationService.CalculateSingleChargeback(loan);
}
catch (Exception e)
{
Log.Error("Failure to Calculate Loan Chargeback: {0} {1}", e.Message, e.StackTrace);
}
return result;
}
public async Task<List<Loan>> SaveLoansAsync(List<Loan> loans)
{
var loansToAddOrUpdate = new List<Loan>();
var historyRecords = new List<AuditHistory>();
var loanTypes = await _loanTypeRepository.GetLoanTypesAsync();
var numberOfLoansToUpdateReserve = 0;
var numberOfLoansToAdd = 0;
var numberOfLoansToUpdate = 0;
try
{
Log.Information($"Have {loans.Count} loans to add or update");
loans.ForEach(loan =>
{
var validationErrors = ValidationHelper.SetValidationErrors(loan);
loan.ActionRequired = validationErrors.Count > 0;
loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId);
var originalLoan = _loanRepository.GetAll().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan != null) //have an updated loan
{
if (!loan.Equals(originalLoan) || (loan.Vehicle != null && !loan.Vehicle.Equals(originalLoan.Vehicle)))
{
numberOfLoansToUpdate++;
loan.LoanId = originalLoan.LoanId;
var needToUpdateReserve = !loan.HasSameReserve(originalLoan);
if (needToUpdateReserve)
{
numberOfLoansToUpdateReserve++;
loan = _loanCalculationService.CalculateLoanReserve(loan, originalLoan.ReserveAmount, originalLoan.ExportedReserveDate);
}
historyRecords.Add(_auditHistoryService.GetExistingLoanUpdateHistoryAsync(loan, originalLoan));
CopyFromDnaLoanToOriginalLoan(loan, originalLoan, needToUpdateReserve);
originalLoan.LoanStatus = LoanStatus.Updated.ToString();
loansToAddOrUpdate.Add(originalLoan);
}
}
else //have a new loan
{
numberOfLoansToAdd++;
loan = _loanCalculationService.CalculateLoanReserve(loan);
loan.Dealer = null;
loan.LoanStatus = LoanStatus.New.ToString();
loansToAddOrUpdate.Add(loan);
}
});
Log.Information($"{numberOfLoansToUpdate} loans were updated");
Log.Information($"{numberOfLoansToUpdateReserve} loans had reserve recalculated");
Log.Information($"{numberOfLoansToAdd} loans were added");
if (loansToAddOrUpdate.Any())
{
await _loanRepository.UpdateAsync(loansToAddOrUpdate);
}
if (historyRecords.Any())
{
await _auditHistoryService.AddHistoriesAsync(historyRecords);
}
}
catch (Exception e)
{
Log.Error("Failure to Save Loans: {0} {1}", e.Message, e.StackTrace);
throw;
}
return loansToAddOrUpdate;
}
public async Task<List<Loan>> SaveWithChargebackAsync(List<Loan> loans)
{
var historiesForClosedLoans = new List<AuditHistory>();
var historiesForLoansNotFound = new List<AuditHistory>();
var loansToClose = new List<Loan>();
var description = "Importing closed loan changed loan status";
loans.ForEach(loan =>
{
var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan != null) //have an updated loan
{
if (!originalLoan.PaidLoanDate.HasValue || originalLoan.PaidLoanDate != loan.PaidLoanDate)
{
var oldStatus = originalLoan.LoanStatus;
originalLoan.PaidLoanDate = loan.PaidLoanDate;
originalLoan.ImportId = loan.ImportId;
var chargebackResult = _loanCalculationService.CalculateSingleChargeback(originalLoan);
AssignChargebackResult(originalLoan, chargebackResult);
historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(originalLoan, oldStatus, description));
loansToClose.Add(originalLoan);
}
}
else //active loan not found to close
{
//set this temporary status to flag selecting original loan with details in order to calculate both reserve and chargeback
loan.LoanStatus = LoanStatus.MissingActiveLoan.ToString();
loansToClose.Add(loan);
historiesForLoansNotFound.Add(_auditHistoryService.GetAuditHistoryForNonexistingClosedLoan(loan));
}
});
await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans);
await _auditHistoryService.AddHistoriesAsync(historiesForLoansNotFound);
await _loanRepository.UpdateAsync(loansToClose.Where(l => l.LoanStatus != "MissingActiveLoan"));
return loansToClose;
}
/// <summary>This method was used for one time import, we want to save the original reserve info for an already closed loan pre Go-Live. </summary>
public async Task<List<Loan>> SaveWithReserveAndChargebackAsync(List<Loan> loans)
{
try
{
var loanTypes = await _loanTypeRepository.GetLoanTypesAsync();
var historiesForClosedLoans = new List<AuditHistory>();
var loansToClose = new List<Loan>();
var description = "Importing closed loan changed loan status";
loans.ForEach(loan =>
{
var originalLoan = _loanRepository.GetAllWithoutDealer().FirstOrDefault(l => l.LoanNumber == loan.LoanNumber);
if (originalLoan == null)
{
loan.LoanType = loanTypes.FirstOrDefault(x => x.LoanTypeId == loan.LoanTypeId);
loan = _loanCalculationService.CalculateLoanReserve(loan, null, null);
var chargebackResult = _loanCalculationService.CalculateSingleChargeback(loan);
AssignChargebackResult(loan, chargebackResult);
loansToClose.Add(loan);
historiesForClosedLoans.Add(_auditHistoryService.GetLoanStatusChangeHistory(loan, "", description));
}
});
await _auditHistoryService.AddHistoriesAsync(historiesForClosedLoans);
await _loanRepository.UpdateAsync(loansToClose);
return loansToClose;
}
catch (Exception ex)
{
Log.Error(ex.Message);
throw;
}
}
public async Task<List<string>> SearchMembers(string searchText)
{
var memberNumbers = new List<string>();
try
{
memberNumbers = await _loanRepository.GetAll().Where(x => x.MemberNumber.StartsWith(searchText))
.Select(y => y.MemberNumber)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of members.");
throw;
}
return memberNumbers.OrderBy(x => long.Parse(x)).ToList();
}
public async Task<List<string>> SearchLoanNumbers(string searchText)
{
var loanNumbers = new List<string>();
try
{
loanNumbers = await _loanRepository.GetAll().Where(x => x.LoanNumber.StartsWith(searchText))
.Select(x => x.LoanNumber)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of loan numbers.");
throw;
}
return loanNumbers.OrderBy(x => long.Parse(x)).ToList();
}
public async Task<List<string>> SearchApplicationIds(string searchText)
{
var applicationIds = new List<string>();
try
{
applicationIds = await _loanRepository.GetAll().Where(x => x.ApplicationId.StartsWith(searchText))
.Select(x => x.ApplicationId)
.Distinct()
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving list of application ids.");
throw;
}
return applicationIds.OrderBy(x => long.Parse(x)).ToList();
}
public async Task UpdateDealerOriginalBalanceAsync(List<string> accountNumbers)
{
foreach(var account in accountNumbers)
{
var exportResult = await _coreApiServiceClient.PutDealerOriginalBalance(account);
if (exportResult != null)
{
foreach (var result in exportResult)
if (!string.IsNullOrEmpty(result.ErrorMessage))
{
Log.Error($"Failure to update dealer original balance loan: {result.AccountNumber} {result.ErrorMessage} ");
}
}
else //if nothing is returned all updates succeeded
{
Log.Information($"Update to dealers original balances completed successfully");
}
}
}
private static void AssignChargebackResult(Loan loan, LoanChargeback chargebackResult, bool skipExport = false)
{
loan.ChargeBack = Math.Round(chargebackResult.ChargebackAmount, 4);
loan.ChargebackMessage = chargebackResult.ChargebackTypeMessage;
loan.RemainingMonthsAtClose = chargebackResult.RemainingMonthsAtClose;
loan.ActionRequired = false;
//if skipping export then loan will already have ExportedChargebackDate set
loan.ExportedChargebackDate = skipExport ? loan.ExportedChargebackDate : null;
loan.LoanStatus = LoanStatus.Closed.ToString();
loan.Dealer = null;
}
private static void CopyFromDnaLoanToOriginalLoan(Loan loan, Loan originalLoan, bool needToUpdateReserve)
{
originalLoan.ActionRequired = loan.ActionRequired;
originalLoan.ImportId = loan.ImportId;
originalLoan.MemberNumber = loan.MemberNumber;
originalLoan.MemberFirstName = loan.MemberFirstName;
originalLoan.MemberInitial = loan.MemberInitial;
originalLoan.MemberLastName = loan.MemberLastName;
originalLoan.DealerOrganizationNumber = loan.DealerOrganizationNumber;
originalLoan.LoanDate = loan.LoanDate;
originalLoan.PostedDate = loan.PostedDate;
originalLoan.FirstPmtDate = loan.FirstPmtDate;
originalLoan.FinalPmtDate = loan.FinalPmtDate;
originalLoan.LoanAmount = loan.LoanAmount;
originalLoan.BuyRate = loan.BuyRate;
originalLoan.ContractRate = loan.ContractRate;
originalLoan.MonthlyPmt = loan.MonthlyPmt;
originalLoan.LoanTypeId = loan.LoanTypeId;
originalLoan.ApplicationId = loan.ApplicationId;
//suppressing these insurance updates until DNA has correct value
//originalLoan.Warranty = loan.Warranty;
//originalLoan.Gap = loan.Gap;
originalLoan.CreditLife = loan.CreditLife;
originalLoan.CreditDisability = loan.CreditDisability;
//originalLoan.Mbp = loan.Mbp;
originalLoan.AccountType = loan.AccountType;
if (originalLoan.Vehicle != null)
{
originalLoan.Vehicle.VINNumber = loan.Vehicle?.VINNumber;
originalLoan.Vehicle.Make = loan.Vehicle?.Make;
originalLoan.Vehicle.Model = loan.Vehicle?.Model;
originalLoan.Vehicle.Mileage = loan.Vehicle?.Mileage ?? 0;
originalLoan.Vehicle.Year = loan.Vehicle?.Year;
originalLoan.Vehicle.Color = loan.Vehicle?.Color;
}
if (needToUpdateReserve)
{
originalLoan.ReserveAmount = loan.ReserveAmount;
originalLoan.ReserveMessage = loan.ReserveMessage;
originalLoan.ExportedReserveDate = loan.ExportedReserveDate;
originalLoan.Term = loan.Term;
originalLoan.FlatFeePaid = loan.FlatFeePaid;
originalLoan.Adjustment = loan.Adjustment;
}
}
}
}
@@ -0,0 +1,9 @@
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services.QueueServices
{
public interface IQueueRmsActiveLoanService
{
Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request);
}
}
@@ -0,0 +1,36 @@
using LCU.Library.Internal.LcuMassTransitModels;
using MassTransit;
using Newtonsoft.Json;
using ReserveManagementSystemApi.Configurations;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services.QueueServices
{
public class QueueRmsActiveLoanService : IQueueRmsActiveLoanService
{
private readonly ISendEndpoint _sendEndpoint;
private readonly ReceiveEndpointConfiguration _rmsActiveLoanImportReceiveEndpoint;
public QueueRmsActiveLoanService(IBusControl busControl, ReceiveEndpointConfigurations receiveEndpointConfigurations)
{
_rmsActiveLoanImportReceiveEndpoint = receiveEndpointConfigurations.RmsActiveLoanReceiveEndpoint;
_sendEndpoint = busControl.GetSendEndpoint(_rmsActiveLoanImportReceiveEndpoint.ReceiveEndpointUri).Result;
}
public async Task QueueRmsActiveLoanServiceRequest(RmsActiveImportRequest request)
{
try
{
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await _sendEndpoint.Send(request, cancellationTokenSource.Token);
}
catch (Exception exception)
{
var message = "Failed to Queue RmsActiveLoanImport Request. " +
$"RmsActiveLoanImport: {JsonConvert.SerializeObject(request)}";
Log.Error(exception, message);
throw new ServiceException($"{message}, Exception Message: {exception.Message}");
}
}
}
}
@@ -0,0 +1,31 @@
using ReserveManagementSystemApi.Services.QueueServices;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemApi.Services
{
public class RmsActiveLoanService : IRmsActiveLoanService
{
private readonly IQueueRmsActiveLoanService _queueRmsActiveLoanService;
private readonly IImportReadService _importReadService;
public RmsActiveLoanService(
IQueueRmsActiveLoanService queueRmsActiveLoanService, IImportReadService importReadService
)
{
_queueRmsActiveLoanService = queueRmsActiveLoanService;
_importReadService = importReadService;
}
public async Task HandleRmsActiveImportRequest(RmsActiveImportRequest request)
{
try
{
await _importReadService.ImportActiveLoansFromDNA(request.ImportDateTime, request.SkipExport);
}
catch (Exception ex)
{
Log.Error(ex, $"Failure importing Active Loans from DNA");
throw new ServiceException($"Exception Message: {ex.Message} - Failure importing Active Loans from DNA");
}
}
}
}
@@ -0,0 +1,42 @@
namespace ReserveManagementSystemApi.Services
{
public class ScheduleService : IScheduleService
{
private readonly IImportReadService _importReadService;
private readonly IExportService _exportService;
public ScheduleService(IImportReadService importReadService, IExportService exportService)
{
_importReadService = importReadService;
_exportService = exportService;
}
public async Task RunImportExportActive(CancellationToken cancellationToken)
{
try
{
Log.Information("Importing active loans from DNA");
var activeImport = await _importReadService.ImportActiveLoansFromDNA(string.Empty);
Log.Information($"{activeImport.RecordsSaved} active loans were saved");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to perform scheduled active loan import.");
}
}
public async Task RunImportExportClosed(CancellationToken cancellationToken)
{
try
{
Log.Information("Importing closed loans from DNA");
var closedImport = await _importReadService.ImportClosedLoansFromDNA(string.Empty);
Log.Information($"{closedImport.RecordsSaved} loans were closed");
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to perform scheduled closed loan import.");
}
}
}
}
@@ -0,0 +1,28 @@
using LCU.Library.Internal.Status.Services;
using LCU.Library.Internal.StatusModels.Models;
namespace ReserveManagementSystemApi.Services;
public class StatusService : IStatusService
{
private readonly IEnvironmentViewService _environmentViewService;
public StatusService(IEnvironmentViewService environmentViewService)
{
_environmentViewService = environmentViewService;
}
public EnvironmentView GetEnviromentView()
{
try
{
var environmentView = _environmentViewService.BuildEnvironmentView();
Log.Verbose("Status Environment View Heartbeat. EnvironmentView: {@EnvironmentView}", environmentView);
return environmentView;
}
catch (Exception exception)
{
Log.Error(exception, $"Failure Getting Enviroment View Information.");
throw new ServiceException($"Failure Getting Enviroment View Information.");
}
}
}
@@ -0,0 +1,29 @@
using ReserveManagementSystemLib.Models;
using ReserveManagementSystemLib.Repositories;
namespace ReserveManagementSystemApi.Services
{
public class VehicleService: IVehicleService
{
private readonly IVehicleRepository _vehicleRepository;
public VehicleService(IVehicleRepository vehicleRepository)
{
_vehicleRepository = vehicleRepository;
}
public async Task<Vehicle> GetVehicleByLoanIdAsync(int? loanId)
{
try
{
return await _vehicleRepository.FindByLoanIdAsync(loanId);
}
catch (Exception ex)
{
Log.Error(ex, "Failure retrieving vehicle by loan id.");
throw;
}
}
}
}
@@ -0,0 +1,8 @@
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.EntityFrameworkCore;
global using Serilog;
global using System;
global using System.Linq;
global using System.Threading.Tasks;
global using LCU.Library.Internal.Exceptions;
@@ -0,0 +1,59 @@
{
"Primary": {
"AppName": "Reserve Management System",
"SeqUrl": "http://localhost",
"SeqApiKey": "VALUE-IN-KEYVAULT",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
],
"FeatureFlags": {
"RunRmsActiveLoanConsumer": true,
"EnableImportExportActive": false,
"EnableImportExportClosed": false
}
},
"SqlConnectionConfiguration": {
"ConnectionString": "data source=localhost;initial catalog=ReserveManagementSystem;Integrated Security=SSPI;MultipleActiveResultSets=True;App=EntityFramework",
"MaxRetryCount": 3,
"MaxRetryDelay": "00:00:30"
},
"ConnectionConfiguration": {
"HostUrl": "localhost",
"UserName": "guest",
"Password": "guest"
},
"RmsActiveLoanReceiveEndpoint": {
"ReceiveEndpoint": "LCU.ReserveManagementSystem.ActiveImport",
"ReceiveEndpointType": "exchange",
"PrefetchLimit": 1
},
"ApiSecurityConfiguration": {
"ApiKeyAuthenticationFailureBehavior": "Error",
"InternalApiKeySet": {
"ReserveManagementSystem": "VALUE-IN-KEYVAULT"
},
"ExternalApiKeySet": {}
},
"CoreApiConfiguration": {
"ApiTimeOutInSeconds": 420,
"BaseUrl": "https://localhost:59303",
"GetIndirectLoansByPostedDate": "/api/core/IndirectLoan/",
"GetClosedLoansByPostedDate": "/api/core/IndirectLoan/Closed/",
"GetClosedIndirectLoansWithDetailByPostedDate": "/api/core/IndirectLoan/ClosedWithDetail/",
"PostMonetaryTransactions": "/api/core/Transaction/PostMonetaryTransactions",
"PutDealerOriginalBalance": "/api/core/IndirectLoan/PutDealerOriginalBalance"
},
"AllowedHosts": "*",
"HostedServices": {
"ImportExportActiveCronSchedule": "0 19 * * 0-6",
"ImportExportClosedCronSchedule": "15 19 * * 0-6"
},
"RmsApiConfiguration": {
"DefaultImportStartDate": "2021-07-01 00:00:00",
"DefaultBatchSize": 100,
"DefaultConvertedLoanPostDate": "2021-10-31"
}
}
@@ -0,0 +1,14 @@
namespace ReserveManagementSystemLib.Configuration
{
public class CoreApiConfiguration
{
public string ClientName { get; set; }
public string ApiTimeOutInSeconds { get; set; }
public string BaseUrl { get; set; }
public string GetIndirectLoansByPostedDate { get; set; }
public string GetClosedLoansByPostedDate { get; set; }
public string GetClosedIndirectLoansWithDetailByPostedDate { get; set; }
public string PostMonetaryTransactions { get; set; }
public string PutDealerOriginalBalance { get; set; }
}
}
@@ -0,0 +1,10 @@
namespace ReserveManagementSystemLib.Configuration
{
public class RmsApiConfiguration
{
public string DefaultImportStartDate { get; set; }
public int DefaultBatchSize { get; set; }
public string DefaultConvertedLoanPostDate { get; set; }
}
}
@@ -0,0 +1,8 @@
namespace ReserveManagementSystemLib.Enums
{
public enum ImportType
{
Active,
Closed
}
}
@@ -0,0 +1,17 @@
namespace ReserveManagementSystemLib.Helpers
{
public static class StringExtensions
{
public static string Truncate(this string inputString, int numberOfCharacters)
{
if (inputString.Length <= numberOfCharacters)
{
return inputString;
}
else
{
return inputString.Substring(0, numberOfCharacters);
}
}
}
}
@@ -0,0 +1,34 @@
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Components.Forms;
using ReserveManagementSystemLib.Models;
namespace ReserveManagementSystemLib.Helpers
{
public class ValidationHelper
{
public static List<ValidationFailure> SetValidationErrors(Loan loanInfo)
{
var validator = GetValidatorForModel();
var editContext = new EditContext(loanInfo);
var context = new ValidationContext<object>(editContext.Model);
var validationResults = validator.Validate(context);
return new List<ValidationFailure>(validationResults.Errors);
}
private static IValidator GetValidatorForModel()
{
LoanValidator baseclass = new LoanValidator();
var modelValidatorInstance = (IValidator)Activator.CreateInstance(baseclass.GetType());
return modelValidatorInstance;
}
}
}
@@ -0,0 +1,18 @@
namespace ReserveManagementSystemLib.Models
{
public class AuditHistory
{
[Key]
public int AuditId { get; set; }
public int? LoanId { get; set; }
public string MemberNumber { get; set; }
public string LoanNumber { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
public string ActivityType { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public string AuditDescription { get; set; }
}
}
@@ -0,0 +1,33 @@
namespace ReserveManagementSystemLib.Models
{
public class CalculationConfig
{
public readonly Loan _loanData;
public CalculationConfig(Loan loanData)
{
_loanData = loanData;
}
public Loan LoanData() { return _loanData; }
public decimal BuydownRateDifference() { return _loanData.BuyRate - _loanData.ContractRate; }
public decimal BuyFinanceCharge { get; set; }
public decimal ContractFinanceCharge { get; set; }
public int DaysToFirstPmt() { return (_loanData.FirstPmtDate - _loanData.LoanDate).Days - 30 > 0 ? (_loanData.FirstPmtDate - _loanData.LoanDate).Days - 30 : 0; }
public decimal ReserveDiscount() { return _loanData.LoanType?.ReserveDiscount ?? 25M; }
public decimal FlatFeePercent() { return _loanData.LoanType?.FlatFeePercent ?? 1M; }
public decimal NoReserveThreshold() { return _loanData.LoanType?.NoReserveThreshold ?? 0.25M; }
public decimal StdRate() { return (_loanData.ContractRate - _loanData.BuyRate); }
}
}
@@ -0,0 +1,32 @@
namespace ReserveManagementSystemLib.Models
{
public class Dealer
{
public string DealerCode { get; set; }
string _dealerName;
public string Name { get { return _dealerName?.ToUpper(); } set => _dealerName = value?.ToUpper(); }
public string DealerGroupCode { get; set; }
public string DealerAccountNumber { get; set; }
[Key]
public string DealerOrganizationNumber { get; set; }
public override bool Equals(object obj)
{
return obj is Dealer dealer &&
DealerCode == dealer.DealerCode &&
Name == dealer.Name?.ToUpper() &&
DealerAccountNumber == dealer.DealerAccountNumber &&
DealerOrganizationNumber == dealer.DealerOrganizationNumber;
}
public override int GetHashCode()
{
return HashCode.Combine(DealerCode, DealerAccountNumber, DealerOrganizationNumber);
}
}
}
@@ -0,0 +1,22 @@
namespace ReserveManagementSystemLib.Models
{
public class Import
{
[Key]
public int ImportId { get; set; }
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public int RecordCount { get; set; }
public int RecordsSaved { get; set; }
public bool Success { get; set; }
public string FileName { get; set; }
public string ImportType { get; set; }
}
}
@@ -0,0 +1,204 @@
using FluentValidation;
using FluentValidation.Results;
using System.ComponentModel.DataAnnotations.Schema;
namespace ReserveManagementSystemLib.Models
{
public class Loan
{
[Key]
public int LoanId { get; set; }
public int ImportId { get; set; }
public string MemberNumber { get; set; }
public string LoanNumber { get; set; }
public string MemberLastName { get; set; }
public string MemberFirstName { get; set; }
public string MemberInitial { get; set; }
public string DealerOrganizationNumber { get; set; }
public DateTime LoanDate { get; set; }
public DateTime PostedDate { get; set; }
public DateTime FirstPmtDate { get; set; }
public DateTime FinalPmtDate { get; set; }
public decimal LoanAmount { get; set; }
public decimal BuyRate { get; set; }
public decimal ContractRate { get; set; }
public decimal MonthlyPmt { get; set; }
public string LoanTypeId { get; set; }
public int Term { get; set; }
public decimal ReserveAmount { get; set; }
public bool FlatFeePaid { get; set; }
public decimal? ChargeBack { get; set; }
public string ChargebackMessage { get; set; }
public byte? RemainingMonthsAtClose { get; set; }
public DateTime? PaidLoanDate { get; set; }
public string ReserveMessage { get; set; }
public bool ActionRequired { get; set; }
public DateTime? StatementDate { get; set; }
public decimal? Adjustment { get; set; }
public string LoanStatus { get; set; }
public DateTime? ExportedReserveDate { get; set; }
public DateTime? ExportedChargebackDate { get; set; }
public string ApplicationId { get; set; }
public bool Warranty { get; set; }
public bool Gap { get; set; }
public bool CreditLife { get; set; }
public bool CreditDisability { get; set; }
public bool Mbp { get; set; }
public string AccountType { get; set; }
[ForeignKey("DealerOrganizationNumber")]
public Dealer Dealer { get; set; }
[ForeignKey("LoanTypeId")]
public LoanType LoanType { get; set; }
public Vehicle Vehicle { get; set; }
[NotMapped]
public List<ValidationFailure> ValidationErrors { get; set; }
public bool HasSameReserve(object obj)
{
return obj is Loan loan &&
LoanDate == loan.LoanDate &&
Term == loan.Term &&
FirstPmtDate == loan.FirstPmtDate &&
FinalPmtDate == loan.FinalPmtDate &&
LoanAmount == loan.LoanAmount &&
LoanTypeId == loan.LoanTypeId &&
BuyRate == loan.BuyRate &&
ContractRate == loan.ContractRate;
}
public bool IsBoatLoan(object obj)
{
return obj is Loan loan &&
AccountType == "DL09" || AccountType == "DL10" || AccountType == "DL11" || AccountType == "DL12";
}
public override bool Equals(object obj)
{
return obj is Loan loan &&
MemberNumber == loan.MemberNumber &&
LoanNumber == loan.LoanNumber &&
MemberLastName == loan.MemberLastName &&
MemberFirstName == loan.MemberFirstName &&
MemberInitial == loan.MemberInitial &&
DealerOrganizationNumber == loan.DealerOrganizationNumber &&
LoanDate == loan.LoanDate &&
PostedDate == loan.PostedDate &&
Term == loan.Term &&
FirstPmtDate == loan.FirstPmtDate &&
FinalPmtDate == loan.FinalPmtDate &&
LoanAmount == loan.LoanAmount &&
BuyRate == loan.BuyRate &&
ContractRate == loan.ContractRate &&
MonthlyPmt == loan.MonthlyPmt &&
LoanTypeId == loan.LoanTypeId &&
ApplicationId == loan.ApplicationId &&
Warranty == loan.Warranty &&
Gap == loan.Gap &&
Mbp == loan.Mbp &&
CreditLife == loan.CreditLife &&
CreditDisability == loan.CreditDisability &&
ActionRequired == loan.ActionRequired;
}
public override int GetHashCode()
{
HashCode hash = new HashCode();
hash.Add(MemberNumber);
hash.Add(LoanNumber);
hash.Add(MemberLastName);
hash.Add(MemberFirstName);
hash.Add(MemberInitial);
hash.Add(DealerOrganizationNumber);
hash.Add(LoanDate);
hash.Add(PostedDate);
hash.Add(FirstPmtDate);
hash.Add(FinalPmtDate);
hash.Add(LoanAmount);
hash.Add(BuyRate);
hash.Add(ContractRate);
hash.Add(MonthlyPmt);
hash.Add(LoanTypeId);
hash.Add(ApplicationId);
hash.Add(Warranty);
hash.Add(Gap);
hash.Add(Mbp);
hash.Add(CreditLife);
hash.Add(CreditDisability);
hash.Add(ActionRequired);
return hash.ToHashCode();
}
}
public class LoanValidator : AbstractValidator<Loan>
{
public LoanValidator()
{
RuleFor(m => m.LoanTypeId).NotEmpty().WithMessage("Loan type cannot be empty!");
RuleFor(m => m.DealerOrganizationNumber).NotEmpty().WithMessage("Dealer Organization Number cannot be empty!");
When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.DealerCode), () =>
{
RuleFor(m => m.Dealer.DealerCode).NotEmpty().WithMessage("Dealer Code cannot be empty!");
});
When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.DealerAccountNumber), () =>
{
RuleFor(m => m.Dealer.DealerAccountNumber).NotEmpty().WithMessage("Dealer Account Number cannot be empty!");
});
When(m => m.Dealer != null && string.IsNullOrEmpty(m.Dealer.Name), () =>
{
RuleFor(m => m.Dealer.Name).NotEmpty().WithMessage("Dealer Name cannot be empty!");
});
When(m => m.Vehicle != null && string.IsNullOrEmpty(m.Vehicle.VINNumber), () =>
{
RuleFor(m => m.Vehicle.VINNumber).NotEmpty().WithMessage("VIN cannot be empty!");
});
RuleFor(m => m.LoanDate).LessThanOrEqualTo(m => m.FirstPmtDate).WithMessage("First Payment Date cannot be before Loan Date!");
}
}
}
@@ -0,0 +1,11 @@
namespace ReserveManagementSystemLib.Models
{
public class LoanChargeback
{
public decimal ChargebackAmount { get; set; }
public string ChargebackTypeMessage { get; set; }
public byte? RemainingMonthsAtClose { get; set; }
}
}
@@ -0,0 +1,22 @@
namespace ReserveManagementSystemLib.Models
{
public class LoanChargebackInput
{
public DateTime LoanDate { get; set; }
public DateTime FirstPmtDate { get; set; }
public DateTime FinalPmtDate { get; set; }
public string LoanType { get; set; }
public decimal ReserveAmount { get; set; }
public DateTime? PaidLoanDate { get; set; }
public int Term { get; set; }
public string AccountType { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More