using FindAndDrive.Domain.Interfaces.Services; using FindAndDrive.Domain.Models.DTO.Request; using FindAndDrive.Domain.Models.DTO.Response; using FindAndDrive.Domain.Interfaces.Gateways; using FindAndDrive.Domain.Models.Gateways.Experian; using FindAndDrive.Domain.Interfaces.Repositories; using FindAndDrive.Domain.Models.Entities; using System.Text.RegularExpressions; using FindAndDrive.Domain.Models.DTO; using FindAndDrive.Domain.Models.Enums; using FindAndDrive.Domain.Models.Settings; using Microsoft.Extensions.Options; using FindAndDrive.Domain.Interfaces.Providers; using FindAndDrive.Domain.Models.Exceptions; using FindAndDrive.Domain.Models.Gateways.Transunion; using System.Globalization; namespace FindAndDrive.Domain.Services; public class FinancingService : IFinancingService { private readonly IExperianGateway _experianGateway; private readonly ITransUnionGateway _transUnionGateway; private readonly IDateTimeProvider _dateTimeProvider; private readonly IApplicantRepository _applicantRepository; private readonly IApplicantFinanceRepository _applicantFinanceRepository; private readonly IApplicantPredictionRepository _applicantPredictionRepository; private readonly IApplicantFraudAlertRepository _applicantFraudAlertRepository; private readonly IApplicantAuditLogRepository _applicantAuditLogRepository; private readonly IApplicantCreditRepository _applicantCreditRepository; private readonly ExperianSettings _experianSettings; private readonly TransUnionSettings _transunionSettings; public FinancingService(IExperianGateway experianGateway, ITransUnionGateway transUnionGateway, IDateTimeProvider dateTimeProvider, IApplicantRepository applicantRepository, IApplicantFinanceRepository applicantFinanceRepository, IApplicantAuditLogRepository applicantAuditLogRepository, IApplicantCreditRepository applicantCreditRepository, IApplicantFraudAlertRepository applicantFraudAlertRepository, IApplicantPredictionRepository applicantPredictionRepository, IOptions experianSettings, IOptions transunionSettings) { _dateTimeProvider = dateTimeProvider; _experianGateway = experianGateway; _transUnionGateway = transUnionGateway; _applicantRepository = applicantRepository; _applicantFinanceRepository = applicantFinanceRepository; _applicantAuditLogRepository = applicantAuditLogRepository; _applicantCreditRepository = applicantCreditRepository; _applicantFraudAlertRepository = applicantFraudAlertRepository; _applicantPredictionRepository = applicantPredictionRepository; _experianSettings = experianSettings.Value; _transunionSettings = transunionSettings.Value; } public async Task FinancingCalc(PreVettingRequest financingData, Guid clientId) { var validateMobile = "^(\\+27)[6-8][0-9]{8}$"; var match = Regex.Match(financingData.MobileNumber, validateMobile, RegexOptions.IgnoreCase); if (!match.Success) { throw new BadRequestException("Invalid South African mobile number"); } var amounts = getMonthlyandTotalAmounts(financingData.NetIncome, financingData.Deposit); var totalAmount = amounts.Total; var monthly = amounts.Monthly; //Create initial applicant object var applicantFinance = new ApplicantFinance { MonthlyFinancingAmount = monthly, TotalFinancingAmount = totalAmount, NetIncome = financingData.NetIncome, Deposit = financingData.Deposit.Value, }; var applicant = new Applicant { FirstName = financingData.FirstName, LastName = financingData.LastName, MobileNumber = financingData.MobileNumber, ClientId = clientId, ApplicantFinance = applicantFinance }; // applicantFinance.Applicant = applicant; //Create applicant first so that we can retrieve the ApplicantId //Call applicant repository, pass in applicant object //In repository save applicant to database, return the ApplicantId (Guid) var applicantId = await _applicantRepository.Create(applicant); var response = new PreVettingResponse { Message = "OK", StatusCode = 200, MonthlyAmount = monthly, TotalAmount = totalAmount, ApplicantId = applicantId }; return response; } private MonthlyAndTotal getMonthlyandTotalAmounts(decimal netIncome, decimal? deposit) { var Total = Math.Round((decimal)((netIncome * (30m / 100m) * (70m / 100m) * 48.4m + deposit) / 1000)) * 1000; var Monthly = (int)(Total / 44.4351m); var response = new MonthlyAndTotal { Monthly = Monthly, Total = Total, }; return response; } public async Task Analytics(AnalyticsRequest request, Guid clientId, Guid apiKey) { var preexistingPrediction = _applicantPredictionRepository.Find(x => x.ApplicantId == request.ApplicantId); if (preexistingPrediction != null && preexistingPrediction.Any()) { throw new PredictionAlreadyExistsException("Prediction has already been calculated"); } var idPattern = "(((\\d{2}((0[13578]|1[02])(0[1-9]|[12]\\d|3[01])|(0[13456789]|1[012])" + "(0[1-9]|[12]\\d|30)|02(0[1-9]|1\\d|2[0-8])))|([02468][048]|[13579][26])0229))" + "(( |-)(\\d{4})( |-)(\\d{3})|(\\d{7}))"; // Regex idcheck = new Regex(idpattern); var match = Regex.Match(request.IDNumber, idPattern, RegexOptions.IgnoreCase); if (!match.Success) { // does not match throw new BadRequestException("Invalid Id number"); } if (request.NetIncome > 999999999) { throw new BadRequestException("NetIncome must be less than 10 digits"); } if (request.NetIncome < 0) { throw new BadRequestException("NetIncome must be a positive number"); } if (request.GrossIncome < 0) { throw new BadRequestException("GrossIncome must be a positive number"); } var applicantData = await _applicantRepository.Get(request.ApplicantId, clientId); var experianRequest = new ExperianRequest { pName = applicantData.FirstName, pSurname = applicantData.LastName, pIdNumber = request.IDNumber, pGrossIncomeAmount = request.GrossIncome, pNetIncomeAmount = request.NetIncome, pLivingExpenses = request.LivingExpenses, pRequestVersion = "1.0", pOriginVersion = "1.0", pOrigin = "Chrome", pOutputFormat = "json", }; var experianData = await GetAffordability(experianRequest); var experianAmounts = getMonthlyandTotalAmounts(experianData.Net_Income_Amount, applicantData.ApplicantFinance.Deposit); var transunionRequest = new TransUnionRequest { xmlRequest = "\r\n\r\n \r\n " + " \r\n false\r\n " + " true\r\n " + _transunionSettings.TransUnionUsername + "\r\n " + " " + _transunionSettings.TransUnionPassword + "\r\n " + applicantData.FirstName + "\r\n " + " " + applicantData.LastName + "\r\n " + request.IDNumber + "\r\n " + " " + applicantData.MobileNumber + "\r\n " + request.EmailAddress + "\r\n " + " " + _transunionSettings.ApiKey + "\r\n " + " \r\n \r\n" }; var transunionData = await DoCreditInfo(transunionRequest); //int.TryParse(transunionData.Credit_Score, out int creditScore); //int.TryParse(transunionData.Band, out int band); var predictionData = new PredictionRequest { CalcExpenses = experianData.Calc_Expense, NetIncome = experianData.Net_Income_Amount, Reason1 = transunionData.TransUnionCreditScore.Reason1, CreditScore = Int32.Parse(transunionData.TransUnionCreditScore.Credit_Score), ScoreIndicator = transunionData.TransUnionCreditScore.ScoreInd }; var prediction = MakePrediction(predictionData); var applicant = new Applicant { ApplicantId = applicantData.ApplicantId, FirstName = applicantData.FirstName, LastName = applicantData.LastName, MobileNumber = applicantData.MobileNumber, ClientId = clientId, ApplicantFinance = applicantData.ApplicantFinance, IdNumber = request.IDNumber, EmailAddress = request.EmailAddress, Title = transunionData.TransUnionConsumerInfo.Title, Gender = transunionData.TransUnionConsumerInfo.Gender, DateOfBirth = DateTime.ParseExact(transunionData.TransUnionConsumerInfo.DateOfBirth, "yyyymmdd", CultureInfo.InvariantCulture), MaritalStatus = transunionData.TransUnionConsumerInfo.MaritalStatusCode, SpouseName = transunionData.TransUnionConsumerInfo.SpouseName ?? "", AddressLine1 = transunionData.TransUnionAddress.Line1, AddressLine2 = transunionData.TransUnionAddress.Line2, Suburb = transunionData.TransUnionAddress.Suburb, City = transunionData.TransUnionAddress.City, PostalCode = transunionData.TransUnionAddress.PostalCode, Province = transunionData.TransUnionAddress.Province, ResidentialStatus = transunionData.TransUnionAddress.OwnerTenant, AddressDate = DateTime.ParseExact(transunionData.TransUnionAddress.InformationDate, "yyyymmdd", CultureInfo.InvariantCulture) }; //ApplicantId doesnt exist error (TODO) await _applicantRepository.Update(applicant); var predictionVariables = GetPredicationVariables(experianData.Bureau_Expenses, experianData.Cal_Net_Income_Amount); var applicantFinance = new ApplicantFinance { ApplicantId = applicant.ApplicantId, MonthlyFinancingAmount = experianAmounts.Monthly, TotalFinancingAmount = experianAmounts.Total, NetIncome = experianData.Net_Income_Amount, Deposit = applicantData.ApplicantFinance.Deposit, BureauExpenses = experianData.Bureau_Expenses, CalculatedGross = experianData.Cal_Gross_Income_Amount, CalculatedNet = experianData.Cal_Net_Income_Amount, CalculatedTotalExpenses = experianData.Calc_Expense, CalculatedLivingExpenses = experianData.Calc_Living_Expenses, PredictorConfidence = experianData.GMIP_Confidence_Level, DisposableIncome = Math.Round(predictionVariables.PtiRatio * experianData.Cal_Net_Income_Amount, 0), GrossIncome = experianData.Gross_Income_Amount, LivingExpenses = experianData.Expenses }; await _applicantFinanceRepository.Update(applicantFinance); var applicantAuditLog = new ApplicantAuditLog { ApplicantId = request.ApplicantId, ExperianId = _experianSettings.ExperianID, AuditLog = transunionData.AuditLog, TimeStamp = _dateTimeProvider.GetSouthAfricanDateTime(), //Move transunion credentials into app settings then pull from app settings. TransUnionId = _transunionSettings.TransUnionUsername, //Apikey of whoever is making the request FindAndDriveId = apiKey }; await _applicantAuditLogRepository.Create(applicantAuditLog); //Need to determine where these values are coming from var applicantCredit = new ApplicantCredit { ApplicantId = request.ApplicantId, ReasonVerbiage = transunionData.TransUnionCreditScore.Reason1Verbiage, CreditScore = Int32.Parse(transunionData.TransUnionCreditScore.Credit_Score), ScoreIndicator = Int32.Parse(transunionData.TransUnionCreditScore.ScoreInd), ReasonCode = transunionData.TransUnionCreditScore.Reason1, ReasonNumber = 1, RiskBand = Int32.Parse(transunionData.TransUnionCreditScore.Band), RiskBankDescription = "" }; await _applicantCreditRepository.Create(applicantCredit); //Income market ranges var incomeMarket = ""; decimal financeSplit = 0.00m; var insuranceSplit = 0.00m; if (7500 < experianData.Net_Income_Amount && experianData.Net_Income_Amount <= 9083) { incomeMarket = "Low Emerging"; financeSplit = 0.7m; insuranceSplit = 0.3m; } if (9084 < experianData.Net_Income_Amount && experianData.Net_Income_Amount <= 19500) { incomeMarket = "Emerging Middle"; financeSplit = 0.7m; insuranceSplit = 0.3m; } if (19501 < experianData.Net_Income_Amount && experianData.Net_Income_Amount <= 35000) { incomeMarket = "Lower Middle"; financeSplit = 0.8m; insuranceSplit = 0.2m; } if (35001 < experianData.Net_Income_Amount && experianData.Net_Income_Amount <= 65250) { incomeMarket = "Upper Middle"; financeSplit = 0.85m; insuranceSplit = 0.15m; } if (65251 < experianData.Net_Income_Amount && experianData.Net_Income_Amount <= 141083) { incomeMarket = "Emerging Affluent"; financeSplit = 0.9m; insuranceSplit = 0.1m; } if (experianData.Net_Income_Amount > 141083) { incomeMarket = "Affluent"; financeSplit = 0.9m; insuranceSplit = 0.1m; } var disparity = ((applicantData.ApplicantFinance.NetIncome - experianData.Cal_Net_Income_Amount) / experianData.Cal_Net_Income_Amount) * 100; var disparityDescription = disparity > 12 ? "Disparity very high" : "No irregularities"; var applicantFraud = new ApplicantFraudAlert { ApplicantId = request.ApplicantId, Disparity = disparity, RatingDescription = incomeMarket, Rating = 1, DisparityDescription = disparityDescription, }; await _applicantFraudAlertRepository.Create(applicantFraud); var financeSpend = Math.Round(applicantFinance.DisposableIncome * financeSplit, 0); var applicantPrediction = new ApplicantPredicition { ApplicantId = request.ApplicantId, ChancesOfApproval = prediction.Prediction.Value, ImprovementSuggestion = prediction.Reason, ImprovementSuggestionFull = prediction.ReasonVerbiage, //IncomeMarket= incomeMarket, //Is this monthly or yearly? (EstimatedApprovalAmount) EstimatedApprovalAmount = financeSpend * 46.7089m, //This is being recalculated based off of information from experian. EstimatedFinanceSpend = financeSpend, //This is being recalculated based off of information from experian. EstimatedInsuranceSpend = applicantFinance.DisposableIncome * insuranceSplit }; await _applicantPredictionRepository.Create(applicantPrediction); return new AnalyticsResponse { StatusCode = 200, Message = "OK", ApplicantId = request.ApplicantId }; } //Third endpoint: Do table joins and returnq all applicant data public async Task GetApplicantById(Guid id, Guid clientId) { var applicant = await _applicantRepository.Get(id, clientId); var response = new ApplicantResponse(); var applicantResponse = new ApplicantResponseReturn { AddressDate = applicant.AddressDate, AddressLine1 = applicant.AddressLine1, AddressLine2 = applicant.AddressLine2, City = applicant.City, ClientId = applicant.ClientId, SpouseName = applicant.SpouseName, Suburb = applicant.Suburb, EmailAddress = applicant.EmailAddress, DateOfBirth = applicant.DateOfBirth, MaritalStatus = applicant.MaritalStatus.ToString(), ResidentialStatus = applicant.ResidentialStatus, FirstName = applicant.FirstName, Gender = applicant.Gender.ToString(), IdNumber = applicant.IdNumber, LastName = applicant.LastName, MobileNumber = applicant.MobileNumber, PostalCode = applicant.PostalCode, Province = applicant.Province.ToString(), Title = applicant.Title, }; var applicantCreditResponse = new ApplicantCreditResponse { ReasonVerbiage = applicant.ApplicantCredit.ReasonVerbiage, }; var applicantFinanceResponse = new ApplicantFinanceResponse { Deposit = applicant.ApplicantFinance.Deposit, GrossIncome = applicant.ApplicantFinance.GrossIncome, LivingExpenses = applicant.ApplicantFinance.LivingExpenses, NetIncome = applicant.ApplicantFinance.NetIncome, ValidatedGross = applicant.ApplicantFinance.CalculatedGross, ValidatedNet = applicant.ApplicantFinance.CalculatedNet, BureauExpenses = applicant.ApplicantFinance.BureauExpenses, VehicleFinanceCalculatedDisposableIncome = applicant.ApplicantFinance.DisposableIncome, }; var applicantFraudAlertResponse = new ApplicantFraudAlertResponse { IncomeDisparityPercentage = applicant.ApplicantFraudAlert.Disparity, }; var applicantPredictionResponse = new ApplicantPredictionResponse { ChancesOfApproval = applicant.ApplicantPredicition.ChancesOfApproval.ToString(), EstimatedApprovalAmount = applicant.ApplicantPredicition.EstimatedApprovalAmount, EstimatedFinanceSpend = applicant.ApplicantPredicition.EstimatedFinanceSpend, EstimatedInsuranceSpend = applicant.ApplicantPredicition.EstimatedInsuranceSpend, ImprovementSuggestion = applicant.ApplicantPredicition.ImprovementSuggestion, ImprovementSuggestionFull = applicant.ApplicantPredicition.ImprovementSuggestionFull, }; var applicantAuditLogResponse = new ApplicantAuditLogResponse { ExperianId = applicant.ApplicantAuditLog.ExperianId, AuditLog = applicant.ApplicantAuditLog.AuditLog, TimeStamp = applicant.ApplicantAuditLog.TimeStamp, TransUnionId = applicant.ApplicantAuditLog.TransUnionId, FindAndDriveId = applicant.ApplicantAuditLog.FindAndDriveId }; response.Applicant = applicantResponse; response.ApplicantFinance = applicantFinanceResponse; response.ApplicantCredit = applicantCreditResponse; response.ApplicantPrediction = applicantPredictionResponse; response.ApplicantFraudAlert = applicantFraudAlertResponse; var responseObj = new GetApplicantByIdResponse { Message = "OK", StatusCode = 200, Response = response }; return responseObj; } public async Task GetAffordability(ExperianRequest request) { return await _experianGateway.getAffordability(request); } public async Task GetAffordabilityIndependent(ExperianRequest request) { return await _experianGateway.getAffordabilityIndependent(request); } public async Task DoCreditInfo(TransUnionRequest request) { return await _transUnionGateway.DoCreditInfo(request); } public async Task DoCreditInfoIndependent(TransUnionRequest request) { return await _transUnionGateway.DoCreditInfoIndependent(request); } public PredictionResponse MakePrediction(PredictionRequest request) { if (request.ScoreIndicator.Length == 1) { request.ScoreIndicator = "0" + request.ScoreIndicator; } if (request.Reason1.Length == 1) { request.Reason1 = "0" + request.Reason1; } var totalIncome = request.NetIncome; var totalExpenses = request.CalcExpenses; var predictionVariables = GetPredicationVariables(totalExpenses, totalIncome); var disposableIncome = (totalIncome * predictionVariables.PtiInt) / 100; var disposableIncomeBool = disposableIncome >= 2250 ? true : false; //Credit score limits //0-730 (above 615 is usually good) var creditScoreBool = true; if (request.CreditScore < 614 || request.CreditScore == null) { creditScoreBool = false; } //Conditions from FindAndDrive predictive model var scoreList1 = new List() { "01", "02", "03" }; var scoreList2 = new List() { "02", "03" }; var reasonList1 = new List() { "07", "09", "19" }; var reasonList2 = new List() { "01", "02", "03", "04", "05", "06", "08", "10", "11", "12", "21", "22", "23", "24", "25", "26", "27" }; //Predictions ChancesOfApproval prediction; var reason = ""; var reasonVerbiage = ""; if (request.NetIncome < 7500) { prediction = ChancesOfApproval.Low; reason = "Affordability"; } else if (request.NetIncome > 7500 && request.CreditScore <= 613) { if (scoreList1.Any(s => request.ScoreIndicator != null && request.ScoreIndicator == s) && reasonList1.Any(s => request.Reason1 != null && request.Reason1 == s)) { prediction = ChancesOfApproval.Low; reason = "Credit History"; reasonVerbiage = "TransUnion is unable to provide you with a credit score because your trade information is insufficient."; } else if (scoreList2.Any(s => request.ScoreIndicator == s) && reasonList2.Any(s => request.Reason1 == s)) { prediction = ChancesOfApproval.Low; reason = "Credit Behaviour"; reasonVerbiage = "You have a high amount outstanding on your accounts. A high outstanding balance may signal to potential lenders that you are spending more than you can afford."; } else { prediction = ChancesOfApproval.Low; reason = "Low Credit Score"; } } else if (request.NetIncome > 7500 && request.CreditScore >= 613 && predictionVariables.DtiInt >= 65) { prediction = ChancesOfApproval.Low; reason = "High Debt"; } else if (request.NetIncome > 7500 && request.CreditScore >= 613 && predictionVariables.DtiInt < 90 && disposableIncome < 2250) { prediction = ChancesOfApproval.Low; reason = "Low Affordability"; } else if ((request.NetIncome > 7500) && (disposableIncome > 2250) && (predictionVariables.DtiInt > 40 && predictionVariables.DtiInt <= 65) && (request.CreditScore > 613 && request.CreditScore <= 689)) { prediction = ChancesOfApproval.Medium; } else if ((request.NetIncome > 7500) && (disposableIncome > 2250) && (predictionVariables.DtiInt > 40 && predictionVariables.DtiInt <= 65) && (request.CreditScore >= 689 && request.CreditScore <= 999)) { prediction = ChancesOfApproval.Medium; } else if ((request.NetIncome > 7500) && (disposableIncome > 2250) && (predictionVariables.DtiInt <= 40) && (request.CreditScore >= 689 && request.CreditScore <= 999)) { prediction = ChancesOfApproval.High; } else { prediction = ChancesOfApproval.Undetermined; reason = "Could not make prediction (Could not match case)"; } var response = new PredictionResponse { Prediction = prediction, Reason = reason, ReasonVerbiage = reasonVerbiage }; return response; } public static PredictionVariablesDto GetPredicationVariables(decimal? totalExpenses, decimal? totalIncome) { var DTI = ((totalExpenses / totalIncome) * 100) ?? 0; var DTIInt = Math.Round((decimal) DTI, 0); //if DTI is greater than 64. Then PTI = 100 - DTI. //if 6090 chances of approval will be low. var PTI = 0.00m; if (DTIInt >= 90) { PTI = 100 - DTI; } else if (65 < DTIInt && DTIInt < 90) { PTI = 80 - DTIInt; } else { PTI = 65 - DTI; } if (PTI > 30) { PTI = 30; } var PTIInt = Math.Round((decimal) PTI, 0); if (PTIInt < 0) { PTIInt = 0; } return new PredictionVariablesDto { Dti = DTI, DtiInt = DTIInt, Pti = PTI, PtiRatio = PTIInt / 100, PtiInt = PTIInt, }; } public async Task TransUnionIndependentCall(TransunionIndependentRequest request) { var transunionRequest = new TransUnionRequest { xmlRequest = "\r\n\r\n \r\n " + " \r\n true\r\n " + " true\r\n " + _transunionSettings.TransUnionUsername + "\r\n " + " " + _transunionSettings.TransUnionPassword + "\r\n " + request.FirstName + "\r\n " + " " + request.LastName + "\r\n " + request.IDNumber + "\r\n " + " " + request.MobileNumber + "\r\n " + request.EmailAddress + "\r\n " + " " + _transunionSettings.ApiKey + "\r\n " + " \r\n \r\n" }; var transunionResponse = await DoCreditInfoIndependent(transunionRequest); return transunionResponse; } public async Task ExperianIndependentCall(ExperianIndependentRequest request) { var experianRequest = new ExperianRequest { pName = request.FirstName, pSurname = request.LastName, pIdNumber = request.IDNumber, pGrossIncomeAmount = request.GrossIncome, pNetIncomeAmount = request.NetIncome, pLivingExpenses = request.LivingExpenses, pRequestVersion = "1.0", pOriginVersion = "1.0", pOrigin = "Chrome", pOutputFormat = "json", }; var experianResponse = await GetAffordabilityIndependent(experianRequest); return experianResponse; } public class MonthlyAndTotal { public decimal Monthly { get; set; } public decimal Total { get; set; } } }