first commit

This commit is contained in:
Zeeshaun
2026-03-11 11:18:25 -05:00
commit f3cb730f3b
472 changed files with 53651 additions and 0 deletions
@@ -0,0 +1,139 @@
using System.Reflection;
using System.ServiceModel;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Events;
using CRIF.ACTion.Configuration;
namespace CRIF.ACTion.Services.Crif
{
public class CrifService : ICrifService
{
private ConnectorServiceClient serviceClient;
private readonly IWcfInspectorBehavior WcfInspectorBehavior;
private readonly CrifSettings _settings;
public CrifService(IWcfInspectorBehavior wcfInspectorBehavior,
IOptions<CrifSettings> settings)
{
WcfInspectorBehavior = wcfInspectorBehavior;
_settings = settings.Value;
IsAnyNullOrEmpty(_settings.ApiSettings);
CreateServiceClient();
}
private void CreateServiceClient()
{
Log.Debug("Creating client binding and object");
var binding = new BasicHttpsBinding
{
MaxBufferSize = int.MaxValue,
ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max,
MaxReceivedMessageSize = int.MaxValue,
AllowCookies = true,
SendTimeout = CrifConstants.SubmissionTimeoutInMinutes
};
var endpoint = new EndpointAddress(_settings.ApiSettings.EndpointUrl);
serviceClient = new ConnectorServiceClient(binding, endpoint);
if (Log.IsEnabled(LogEventLevel.Debug))
{
serviceClient.Endpoint.EndpointBehaviors.Add(WcfInspectorBehavior);
}
}
public btMessage BuildMessage(XElement actionApi)
{
Log.Information("Building btMessage");
btMessage message = new btMessage
{
body = actionApi,
routing = new Routing
{
org = new Org
{
orgCode = _settings.ApiSettings.OrgCode,
orgID = _settings.ApiSettings.OrgId,
orgIDSpecified = true,
},
user = new User
{
userName = _settings.ApiSettings.Username,
},
@interface = new Interface
{
interfaceID = _settings.ApiSettings.InterfaceId,
interfaceIDSpecified = true,
},
action = new CRIF.ACTion.Action
{
transactionType = "API",
},
system = new ApproSystem(), // empty object required
approResult = new ApproResult(), // empty object required
}
};
return message;
}
public async Task<XElement> SendMessage(XElement request)
{
var message = BuildMessage(request);
XElement body;
try
{
Log.Information("Sending message to CRIF");
var response = await serviceClient.SubmitExAsync(message);
if (response.routing.approResult.exceptionCodeSpecified)
{
//Note that approResult.result will likely equal ApproResultEnum.Success
//even with a failed submission
var errorResult = response.routing.approResult;
var errorText = "CRIF responded with error " + errorResult.exceptionCode + " - " + errorResult.exceptionText;
if (errorResult.exceptionCode == 13)
{
// Go ahead and catch the error 13 that CRIF won't resolve.
throw new Error13Exception(errorText + "[KnownReoccuringError]");
} else {
throw new FormatException("CRIF responded with error " +
errorResult.exceptionCode + " - " + errorResult.exceptionText);
}
}
body = response.body;
}
catch (Exception e)
{
Log.Debug("{exception}", e.Message);
throw;
}
return body;
}
public static bool IsAnyNullOrEmpty(ApiConnectionSettings myObject)
{
foreach (PropertyInfo pi in myObject.GetType().GetProperties())
{
if (pi.PropertyType == typeof(string))
{
string value = (string)pi.GetValue(myObject);
if (string.IsNullOrEmpty(value))
{
throw new ArgumentNullException(pi.ToString());
}
}
}
return false;
}
}
}
@@ -0,0 +1,265 @@
using Microsoft.Extensions.Options;
using Serilog;
using CRIF.ACTion.Common.Parsers;
using CRIF.ACTion.Configuration;
using CRIF.ACTion.Decisioning;
using CRIF.ACTion.Files;
using CRIF.ACTion.Loan;
using CRIF.ACTion.Loan.Individuals;
using CRIF.ACTion.Services.Landmark.DistributionChannel;
using Landmark.OLA.Models;
using Landmark.OLA.Models.Decisioning;
using Landmark.OLA.Models.Decisioning.Enums;
using Landmark.OLA.Models.Individuals;
using Landmark.OLA.Models.Loan;
namespace CRIF.ACTion.Services.Crif
{
public class CrifVendorClient : ICrifVendorClient
{
private readonly string _getCustomFieldsString = "getCustomFields";
private readonly string _getProductDetailsString = "getProducts";
private readonly int retryLimit = 6;
private readonly ICrifService _crifService;
private readonly IResponseParser<Individual> _individualResponseParser;
private readonly IResponseParser<LoanApplication> _loanApplicationResponseParser;
private readonly IResponseParser<DecisioningState> _decisioningStateResponseParser;
private readonly IResponseParser<DecisioningDetails> _decisioningDetailsResponseParser;
private readonly IResponseParser<SortedList<int, string>> _configurationResponseParser;
private readonly IResponseParser<Dictionary<string, string>> _productDetailsResponseParser;
private readonly IResponseParser<CustomFieldConfigs> _customFieldResponseParser;
private readonly IResponseParser<DealershipAuthRequest> _dealershipResponseParser;
private readonly CrifSettings _settings;
public CrifVendorClient(ICrifService crifService,
IResponseParser<Individual> individualResponseParser,
IResponseParser<LoanApplication> loanApplicationResponseParser,
IResponseParser<DecisioningState> decisioningStateResponseParser,
IResponseParser<DecisioningDetails> decisioningDetailsResponseParser,
IResponseParser<SortedList<int, string>> crifConfigurationResponseParser,
IResponseParser<Dictionary<string, string>> productDetailsResponseParser,
IResponseParser<CustomFieldConfigs> customFieldResponseParser,
IResponseParser<DealershipAuthRequest> dealershipResponseParser,
IOptions<CrifSettings> settings)
{
_crifService = crifService;
_individualResponseParser = individualResponseParser;
_loanApplicationResponseParser = loanApplicationResponseParser;
_decisioningStateResponseParser = decisioningStateResponseParser;
_decisioningDetailsResponseParser = decisioningDetailsResponseParser;
_configurationResponseParser = crifConfigurationResponseParser;
_productDetailsResponseParser = productDetailsResponseParser;
_customFieldResponseParser = customFieldResponseParser;
_dealershipResponseParser = dealershipResponseParser;
_settings = settings.Value;
_settings.CustomFields = GetCustomFieldConfiguration().GetAwaiter().GetResult();
}
public async Task<DealershipAuthRequest> GetDealershipAuthDetails(string applicationId)
{
var getApp = new DealershipRequestBuilder(applicationId, _settings);
var xmlApp = getApp.Build();
try
{
var response = await _crifService.SendMessage(xmlApp);
return _dealershipResponseParser.Parse(response);
}
catch (Exception e)
{
Log.Fatal("Error parsing response: " + e.Message);
throw;
}
}
public async Task<LoanApplication> CreateApplication(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null)
{
Log.Debug("Building Loan Application");
ModelValidator.Validate(loanApplication);
var createApp = new LoanApplicationRequestBuilder(loanApplication, _settings, null, null, null, channelService);
var xmlApp = createApp.Build();
var response = await _crifService.SendMessage(xmlApp);
try
{
return _loanApplicationResponseParser.Parse(response);
}
catch (Exception)
{
Log.Fatal("Error parsing response.");
throw;
}
}
public async Task<string> CreateApplicationAndGetId(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null)
{
var parsedLoanApplication = await CreateApplication(loanApplication, channelService);
return parsedLoanApplication.Id;
}
public async Task<bool> AddFile(string applicationId, LoanApplicationFile file, string fileBase64Value)
{
try
{
var addFile = new FileRequestBuilder(applicationId, file, fileBase64Value, _settings);
var response = await _crifService.SendMessage(addFile.Build());
return true;
}
catch (Exception e)
{
Log.Fatal("Error uploading file to loan application: " + applicationId);
Log.Fatal(e.ToString());
}
return false;
}
public async Task<Individual> AddIndividual(string applicationId, Individual individual)
{
ModelValidator.Validate(individual);
var addIndividual = new AddIndividualRequestBuilder(applicationId, individual, _settings);
var response = await _crifService.SendMessage(addIndividual.Build());
try
{
return _individualResponseParser.Parse(response);
}
catch (Exception)
{
Log.Fatal("Error parsing response.");
throw;
}
}
public async Task<bool> SendToDecisioning(string applicationId)
{
var sendToDecisioning = new SendToDecisioningRequestBuilder(applicationId, _settings);
var response = await _crifService.SendMessage(sendToDecisioning.Build());
//TODO: This cast is not particularly Dependency Injection-y
//TODO: Requiring the Application Id is really just a double check, and may not matter all that much
((DecisioningStateResponseParser)_decisioningStateResponseParser).SetLoanId(applicationId);
var result = _decisioningStateResponseParser.Parse(response);
if (result == DecisioningState.MissingRequiredInformation)
{
throw new InvalidOperationException("Loan application cannot be sent to decisioning, and is likely missing required information.");
}
if (result == DecisioningState.Requested || result == DecisioningState.Ready)
{
return true;
}
else
{
return false;
}
}
public async Task<DecisioningDetails> GetDecisionStatus(string applicationId)
{
//TODO: This cast is not particularly Dependency Injection-y
//TODO: Requiring the Application Id is really just a double check, and may not matter all that much
((GetApplicationProductsResponseParser)_decisioningDetailsResponseParser).SetLoanId(applicationId);
var result = new DecisioningDetails()
{
DecisioningState = DecisioningState.Unknown
};
int retryAttempts = 0;
Thread.Sleep(2500);
do
{
Thread.Sleep((retryAttempts * 200) + 1500);
var getDecisionStatus = new GetApplicationProductsRequestBuilder(applicationId, _settings);
var response = await _crifService.SendMessage(getDecisionStatus.Build());
result = _decisioningDetailsResponseParser.Parse(response);
retryAttempts++;
} while (retryAttempts < retryLimit && result.DecisioningState == DecisioningState.Unknown);
if (retryAttempts >= retryLimit && result.DecisioningState == DecisioningState.Unknown)
{
result.DecisioningState = DecisioningState.Ready;
result.DecisioningResult = DecisioningResult.DecisionStatus_Review;
}
Log.Information($"Application ID: {applicationId}, Number of Attempts to Get DecisionStatus: {retryAttempts}");
return result;
}
public async Task<SortedList<int, string>> GetBranchNameConfiguration(string ConfigurationSectionName)
{
var configurationRequest = new ConfigurationRequestBuilder(ConfigurationSectionName, _settings);
var response = await _crifService.SendMessage(configurationRequest.Build());
var result = _configurationResponseParser.Parse(response);
return result;
}
public async Task<CustomFieldConfigs> GetCustomFieldConfiguration()
{
CustomFieldConfigs customFieldConfig = new CustomFieldConfigs();
string municipalityTypeId = null;
string municipalityWhereKeptId = null;
try
{
var configurationRequest = new ConfigurationRequestBuilder(_getCustomFieldsString, _settings);
var response = await _crifService.SendMessage(configurationRequest.Build());
customFieldConfig = _customFieldResponseParser.Parse(response);
if (_settings?.CustomFieldMappings?.CustomFieldID != null)
{
_settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityType", out municipalityTypeId);
_settings.CustomFieldMappings.CustomFieldID.TryGetValue("MunicipalityWhereKept", out municipalityWhereKeptId);
}
Log.Information(
"CRIF custom field mappings loaded: MunicipalityType={MunicipalityTypeId}, MunicipalityWhereKept={MunicipalityWhereKeptId}",
municipalityTypeId ?? "<missing>",
municipalityWhereKeptId ?? "<missing>");
}
catch (Exception ex)
{
Log.Fatal("Error parsing CRIF custom fields configuration response." + ex.StackTrace);
}
customFieldConfig.ProductIdToCategoryId = await GetProductDetailsConfiguration();
return customFieldConfig;
}
public async Task<Dictionary<string, string>> GetProductDetailsConfiguration()
{
Dictionary<string, string> mappingList = [];
try
{
var configurationRequest = new ConfigurationRequestBuilder(_getProductDetailsString, _settings);
var response = await _crifService.SendMessage(configurationRequest.Build());
mappingList = _productDetailsResponseParser.Parse(response);
}
catch (Exception ex)
{
Log.Fatal("Error parsing CRIF custom fields configuration response." + ex.StackTrace);
}
return mappingList;
}
}
}
@@ -0,0 +1,10 @@
namespace CRIF.ACTion.Services.Crif
{
public class Error13Exception : Exception
{
public Error13Exception(string message) : base(message)
{
}
}
}
@@ -0,0 +1,11 @@
using System.Xml.Linq;
namespace CRIF.ACTion.Services.Crif
{
public interface ICrifService
{
btMessage BuildMessage(XElement actionApi);
Task<XElement> SendMessage(XElement request);
}
}
@@ -0,0 +1,29 @@
using CRIF.ACTion.Configuration;
using CRIF.ACTion.Services.Landmark.DistributionChannel;
using Landmark.OLA.Models.Decisioning;
using Landmark.OLA.Models.Individuals;
using Landmark.OLA.Models.Loan;
namespace CRIF.ACTion.Services.Crif
{
public interface ICrifVendorClient
{
Task<DealershipAuthRequest> GetDealershipAuthDetails(string applicationId);
Task<LoanApplication> CreateApplication(LoanApplication loanApplication, IDistributionChannelLookupService channelService);
Task<string> CreateApplicationAndGetId(LoanApplication loanApplication, IDistributionChannelLookupService channelService = null);
Task<bool> AddFile(string applicationId, LoanApplicationFile file, string fileBase64Value);
Task<Individual> AddIndividual(string applicationId, Individual individual);
Task<bool> SendToDecisioning(string applicationId);
Task<DecisioningDetails> GetDecisionStatus(string applicationId);
Task<SortedList<int, string>> GetBranchNameConfiguration(string ConfigurationSectionName);
Task<CustomFieldConfigs> GetCustomFieldConfiguration();
}
}
@@ -0,0 +1,9 @@
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace CRIF.ACTion.Services.Crif
{
public interface IWcfInspectorBehavior: IClientMessageInspector, IEndpointBehavior
{
}
}
@@ -0,0 +1,46 @@
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using Serilog;
namespace CRIF.ACTion.Services.Crif
{
public class WcfInspectorBehavior : IWcfInspectorBehavior
{
public WcfInspectorBehavior()
{
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
Log.Debug("Reply received: {reply}", reply);
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
Log.Debug("Sending request: {request}", request);
return null;
}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(this);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
}
@@ -0,0 +1,7 @@
namespace CRIF.ACTion.Services.Landmark.DistributionChannel
{
public interface IDistributionChannelLookupService
{
int GetDistributionChannelByZipCode(string zipCode);
}
}
@@ -0,0 +1,23 @@
using CRIF.ACTion.Configuration;
using Landmark.OLA.Models.Loan;
namespace CRIF.ACTion.Services.Landmark.DistributionChannel
{
public class LandmarkDistributionChannelLookupService : IDistributionChannelLookupService
{
private readonly ServicingChannelSettings _settings;
public LandmarkDistributionChannelLookupService(CrifSettings settings)
{
_settings = settings.ServicingChannels;
}
public int GetDistributionChannelByZipCode(string zipCode)
{
var partyId = BranchMapping.BranchCodesByZip.TryGetValue(zipCode, out int foundValue)
? foundValue : _settings.DefaultServicingChannel;
return partyId;
}
}
}
@@ -0,0 +1,38 @@
using CRIF.ACTion.Configuration;
using Landmark.OLA.Models.Loan;
using Landmark.OLA.Models.Loan.Enums;
namespace CRIF.ACTion.Services.Landmark
{
public class ProductIdService
{
private readonly CrifSettings _settings;
public ProductIdService(CrifSettings settings)
{
_settings = settings;
}
public int GetProductId(LoanType loanType)
{
return loanType switch
{
LoanType.CC_CashBack => _settings.ProductIds.CC_CashBack,
LoanType.CC_Rewards => _settings.ProductIds.CC_Rewards,
LoanType.CC_Student => _settings.ProductIds.CC_Student,
LoanType.CC_Secure => _settings.ProductIds.CC_Secure,
LoanType.EQ_HELOC_Flex => _settings.ProductIds.EQ_HELOC_Flex,
LoanType.EQ_Home_Equity_Fixed => _settings.ProductIds.EQ_Home_Equity_Fixed,
LoanType.EQ_Mtg_Refi_Express => _settings.ProductIds.EQ_Mtg_Refi_Express,
LoanType.EQ_Mfg_Home_With_or_Without_Land => _settings.ProductIds.EQ_Mfg_Home_And_Land,
LoanType.V_Vehicle => _settings.ProductIds.V_Vehicle,
LoanType.V_VehiclePreapproval => _settings.ProductIds.V_VehiclePreapproval,
LoanType.M_Boat => _settings.ProductIds.M_Boat,
LoanType.P_Powersport => _settings.ProductIds.P_Powersport,
LoanType.T_TrailerMotorhome => _settings.ProductIds.T_TrailerMotorhome,
LoanType.S_Signature or LoanType.S_Special => _settings.ProductIds.S_Signature,
_ => throw new ArgumentOutOfRangeException(nameof(LoanApplication.LoanType), $"The requested loan type {loanType} is not implemented."),
};
}
}
}
@@ -0,0 +1,9 @@
using Landmark.OLA.Models.Loan.Enums;
namespace CRIF.ACTion.Services.Landmark.SourceChannel
{
public interface ISourceChannelLookupService
{
int GetSourceChannelByLoanType(LoanType loanType);
}
}
@@ -0,0 +1,40 @@
using System.Collections.ObjectModel;
using CRIF.ACTion.Configuration;
using Landmark.OLA.Models.Loan.Enums;
namespace CRIF.ACTion.Services.Landmark.SourceChannel
{
public class LandmarkSourceChannelLookupService : ISourceChannelLookupService
{
private readonly CrifSettings _settings;
private ReadOnlyDictionary<LoanType, int> _loanTypeToSourceChannel;
public LandmarkSourceChannelLookupService(CrifSettings settings)
{
_settings = settings;
ConfigureMapping();
}
private void ConfigureMapping()
{
var mapping = new Dictionary<LoanType, int>
{
{LoanType.CC_CashBack, _settings.SourceChannels.CreditCardSourceChannelId},
{LoanType.CC_Rewards, _settings.SourceChannels.CreditCardSourceChannelId},
{LoanType.CC_Secure, _settings.SourceChannels.CreditCardSourceChannelId},
{LoanType.CC_Student, _settings.SourceChannels.CreditCardSourceChannelId},
};
_loanTypeToSourceChannel = new ReadOnlyDictionary<LoanType, int>(mapping);
}
public int GetSourceChannelByLoanType(LoanType loanType)
{
var channelId = _loanTypeToSourceChannel.TryGetValue(loanType, out int foundValue)
? foundValue : _settings.SourceChannels.DefaultSourceChannelId;
return channelId;
}
}
}