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,20 @@
namespace Landmark.OLA.Models.Individuals
{
public class CoApplicant : Individual
{
public bool IsMarriedToPrimaryBorrower { get; set; }
public CoApplicant()
: base()
{
}
public CoApplicant(Guid id)
: base(id)
{
}
}
}
@@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals
{
public class Employer : OlaModel
{
[Required]
[StringLength(60, MinimumLength = 1)]
//Due to downstream CRIF issues, applications with employer names containing special characters will be denied
[RegularExpression(@"[a-zA-Z0-9 ]*")]
public string Name { get; set; }
[Required]
//Nullable so that there is no default value
public bool? CurrentEmployer { get; set; } // "isCurrentFL"
[Required]
//Nullable so that there is no default value
public EmploymentTypeOption EmploymentType { get; set; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string Title { get; set; }
[Required]
public Income Income { get; set; }
//[Required]
//The current front end offers an upper limit of 10 years, 11 months.
[Range(0, 130)]
public int MonthsEmployed { get; set; }
//[Required]
//Nullable so that there is no default value
public DateTime? StartDate { get; set; }
// Required if CurrentEmployer == false
//Nullable so that there is no default value
public DateTime? EndDate { get; set; }
[MaxLength(180)]
public string WorkPhone { get; set; }
[Required]
//Nullable so that there is no default value
public LanguageOption? Language;
public Employer(Guid? employerId = null)
{
Id = employerId.HasValue ? employerId.Value.ToString() : null;
}
}
}
@@ -0,0 +1,13 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public enum EmploymentTypeOption
{
NoneSelected,
Contract,
FullTime,
PartTime,
SelfEmployed,
Retired,
Temporary
}
}
@@ -0,0 +1,16 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
/// <summary>
/// EthnicityEnum from CRIF's commonSchema.xsd trimmed down to HMDA form options
/// </summary>
public enum Ethnicity
{
HispanicOrLatino,
NotHispanicOrLatino,
Mexican,
PuertoRican,
Cuban,
OtherHispanicLatino,
NotProvided
}
}
@@ -0,0 +1,13 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public enum Frequency
{
Annually,
SemiAnnually,
Monthly,
SemiMonthly,
TwoWeeks,
Weekly,
Hourly
}
}
@@ -0,0 +1,9 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public enum LanguageOption
{
//A language value is required by the CRIF API
//TODO #229: Accept other values here?
English,
}
}
@@ -0,0 +1,8 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public enum PhoneType
{
Mobile,
Home,
}
}
@@ -0,0 +1,26 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
/// <summary>
/// RaceEnum from CRIF's commonSchema.xsd trimmed down to HMDA form options
/// </summary>
public enum Race
{
AmericanIndianAlaskaNative,
Asian,
NativeHawaiian,
BlackOrAfricanAmerican,
White,
AsianIndian,
Chinese,
Filipino,
Japanese,
Korean,
Vietnamese,
OtherAsian,
Hawaiian,
GuamanianChamorro,
Samoan,
OtherPacificIslander,
NotProvided
}
}
@@ -0,0 +1,12 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
/// <summary>
/// Values from HMDA form
/// </summary>
public enum Sex
{
Female,
Male,
NotProvided,
}
}
@@ -0,0 +1,13 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public enum Suffix
{
II,
III,
IV,
V,
Jr,
Sr
}
}
@@ -0,0 +1,45 @@
namespace Landmark.OLA.Models.Individuals.Enums
{
public class SuffixType
{
public string CD { get; set; }
public string Name { get; set; }
public Suffix SuffixEnum { get; set; }
public SuffixType(string cd, string name, Suffix suffix)
{
SuffixEnum = suffix;
CD = cd;
Name = name;
}
}
public sealed class SuffixMap
{
public SuffixMap() {}
public static readonly SuffixType II = new("2", "II", Suffix.II);
public static readonly SuffixType III = new("3", "III", Suffix.III);
public static readonly SuffixType IV = new("4", "IV", Suffix.IV);
public static readonly SuffixType V = new("5", "V", Suffix.V);
public static readonly SuffixType Jr = new("J", "Jr.", Suffix.Jr);
public static readonly SuffixType Sr = new("S", "Sr.", Suffix.Sr);
public static readonly SuffixType[] map = [II, III, IV, V, Jr, Sr];
public static SuffixType GetSuffixType(Suffix? suffix)
{
SuffixType match = null;
foreach (SuffixType suff in map)
{
if (suff.SuffixEnum == suffix)
{
match = suff;
}
}
return match;
}
}
}
@@ -0,0 +1,101 @@
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Individuals.Enums;
using Landmark.OLA.Models.Residences;
namespace Landmark.OLA.Models.Individuals
{
public class HistoryIndividual : IIndividualHistory
{
public Guid Id { get; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string FirstName { get; set; }
[StringLength(1, MinimumLength = 0)]
[RegularExpression(@"[a-zA-Z]")]
public string MiddleInitial { get; set; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string LastName { get; set; }
public Suffix? Suffix { get; set; }
[Required]
public CurrentResidenceAddress Address { get; set; }
public Address MailingAddress { get; set; }
[Required]
//Nullable so that there is no default value
public LanguageOption? Language { get; set; }
//A value is required for decisioning. May require input from user in the future.
[StringLength(60)]
public string CustomerNumber { get; set; } = "0";
[Required]
//Nullable so that there is no default value
public DateTime? DOB { get; set; }
[Required]
[StringLength(9, MinimumLength = 9)]
[RegularExpression(@"[0-9]+")]
public string SSN { get; set; }
[Required]
[StringLength(180, MinimumLength = 10)]
//CRIF accepts up to 180 characters.
//Dashes, periods, and parentheses are accepted but ignored.
//Numbers, letters, pound signs, and spaces are kept.
public string Phone { get; set; }
[Required]
//Nullable so that there is no default value
public PhoneType? PhoneType { get; set; }
[StringLength(180, MinimumLength = 10)]
public string SecondaryPhone { get; set; }
public PhoneType? SecondaryPhoneType { get; set; }
[Required] //Required by Landmark
[StringLength(180, MinimumLength = 3)]
[RegularExpression(@".+@.+")]
//Given that a user could enter a fake address,
//this is not exhaustive check for a valid address form
public string Email { get; set; }
[StringLength(180)]
[RegularExpression(@".+@.+")]
public string PrimaryApplicantEmail { get; set; }
[StringLength(180)]
[RegularExpression(@".+@.+")]
public string CoApplicantEmail { get; set; }
//TODO #228: Enforce time requirement of total MonthsResided across Address (on IndividualBase) and PreviousAddresses
public ICollection<ResidenceAddress> PreviousAddresses { get; set; }
public ICollection<Employer> Employers { get; set; }
public ICollection<Income> OtherIncomes { get; set; }
public bool? IsLandmarkEmployee { get; set; }
public HistoryIndividual()
{
Id = Guid.NewGuid();
}
public HistoryIndividual(Guid id)
{
Id = id;
}
[Required]
public bool HasOptedOutTCPA { get; set; }
}
}
@@ -0,0 +1,17 @@
using Landmark.OLA.Models.HomeMortgageDisclosureAct;
namespace Landmark.OLA.Models.Individuals
{
public interface IApplicant : IIndividualHistory
{
bool? IsMarried { get; set; }
MailableSpouse NonApplicantSpouse { get; set; }
HistoryIndividual DetailedNonApplicantSpouse { get; set; }
Hmda HmdaValues { get; set; }
bool HasAnyNonApplicantSpouse();
}
}
@@ -0,0 +1,7 @@
namespace Landmark.OLA.Models.Individuals
{
public interface ICoApplicant : IApplicant
{
bool IsMarriedToPrimaryBorrower { get; set; }
}
}
@@ -0,0 +1,49 @@
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Individuals.Enums;
using Landmark.OLA.Models.Residences;
namespace Landmark.OLA.Models.Individuals
{
public interface IIndividualHistory : IReferenceable
{
string FirstName { get; set; }
string MiddleInitial { get; set; }
string LastName { get; set; }
Suffix? Suffix { get; set; }
string SSN { get; set; }
DateTime? DOB { get; set; }
string Phone { get; set; }
PhoneType? PhoneType { get; set; }
string SecondaryPhone { get; set; }
PhoneType? SecondaryPhoneType { get; set; }
string Email { get; set; }
LanguageOption? Language { get; set; }
string CustomerNumber { get; set; }
CurrentResidenceAddress Address { get; set; }
ICollection<ResidenceAddress> PreviousAddresses { get; set; }
Address MailingAddress { get; set; }
ICollection<Employer> Employers { get; set; }
ICollection<Income> OtherIncomes { get; set; }
bool? IsLandmarkEmployee { get; set; }
bool HasOptedOutTCPA { get; set; }
}
}
@@ -0,0 +1,24 @@
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals
{
public interface INonApplicant : IReferenceable
{
string FirstName { get; set; }
string MiddleInitial { get; set; }
string LastName { get; set; }
Address MailingAddress { get; set; }
string SSN { get; set; }
string Email { get; set; }
LanguageOption? Language { get; set; }
bool HasOptedOutTCPA { get; set; }
}
}
@@ -0,0 +1,68 @@
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals
{
public class Income
{
[Required]
//Nullable so that there is no default value
public IncomeType? IncomeTypeCD { get; set; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string SourceName { get; set; }
// required based on type?
//Nullable so that there is no default value
public AlimonyStatus? AlimonyStatus { get; set; }
[Required]
[Range(minimum: 0, maximum: 1000000000)]
public decimal? AmountPerPeriod { get; set; }
[Required]
//Nullable so that there is no default value
public Frequency? Frequency { get; set; }
[Range(0, 168)]
public int? HoursPerWeek { get; set; }
[Required]
// Must be Excluded when part of a previous employer
//Nullable so that there is no default value
public IncomeStatus? IncomeStatus { get; set; }
}
public enum IncomeStatus
{
IncomeStatus_Excluded,
IncomeStatus_Included
}
public enum IncomeType
{
Alimony,
Bonuses,
ChildSupport,
RetirementDisability,
Dividends,
Commissions,
Other,
Spouse,
RentalProperty,
Interest,
SocialSecurity,
IndividualAccount,
Salary
}
public enum AlimonyStatus
{
CourtOrdered,
OralUnderstanding,
Written
}
}
@@ -0,0 +1,16 @@
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals
{
public class IncomeLimits
{
private static Limit HourLimit => new() { min = 0, max = 999 };
private static Limit AnnualLimit => new() { min = 0, max = 1000000000 };
public static Dictionary<Frequency, Limit> Limits => new()
{
{ Frequency.Hourly, HourLimit },
{ Frequency.Annually, AnnualLimit },
};
}
}
@@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models.HomeMortgageDisclosureAct;
namespace Landmark.OLA.Models.Individuals
{
public class Individual : HistoryIndividual, IApplicant
{
[Required]
//Nullable so that there is no default value
public bool? IsMarried { get; set; }
public MailableSpouse NonApplicantSpouse { get; set; }
public HistoryIndividual DetailedNonApplicantSpouse { get; set; }
public Hmda HmdaValues { get; set; }
public Individual()
: base()
{
HmdaValues = new Hmda();
}
public Individual(Guid id)
: base(id)
{
HmdaValues = new Hmda();
}
public Individual( Hmda hmda)
: base()
{
HmdaValues = hmda;
}
public bool HasAnyNonApplicantSpouse()
{
var hasNonApplicantSpouse = NonApplicantSpouse is not null;
var hasDetailedNonApplicantSpouse = DetailedNonApplicantSpouse is not null;
return hasNonApplicantSpouse || hasDetailedNonApplicantSpouse;
}
}
}
@@ -0,0 +1,51 @@
using System.ComponentModel.DataAnnotations;
using Landmark.OLA.Models.Addresses;
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals
{
public class MailableSpouse : INonApplicant
{
public Guid Id { get; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string FirstName { get; set; }
[StringLength(1, MinimumLength = 0)]
[RegularExpression(@"[a-zA-Z]")]
public string MiddleInitial { get; set; }
[Required]
[StringLength(60, MinimumLength = 1)]
public string LastName { get; set; }
[Required]
public Address MailingAddress { get; set; }
[StringLength(9, MinimumLength = 9)]
[RegularExpression(@"[0-9]+")]
public string SSN { get; set; }
[StringLength(180, MinimumLength = 3)]
[RegularExpression(@".+@.+")]
public string Email { get; set; }
[Required]
//Nullable so that there is no default value
public LanguageOption? Language { get; set; }
public MailableSpouse()
{
Id = Guid.NewGuid();
}
public MailableSpouse(Guid id)
{
Id = id;
}
[Required]
public bool HasOptedOutTCPA { get; set; }
}
}
@@ -0,0 +1,26 @@
using FluentValidation;
namespace Landmark.OLA.Models.Individuals.Validators
{
class ApplicantValidator : AbstractValidator<IApplicant>
{
public ApplicantValidator()
{
RuleForEach(x => x.Employers).SetValidator(new EmployerValidator());
When(x => x.DetailedNonApplicantSpouse is not null, () =>
{
RuleForEach(x => x.DetailedNonApplicantSpouse.Employers).SetValidator(new EmployerValidator());
});
When(x => x.NonApplicantSpouse is not null, () =>
{
//An individual can only have a single spouse entry
RuleFor(x => x.DetailedNonApplicantSpouse).Null();
});
RuleFor(x => x.SecondaryPhoneType).NotNull().When(x => x.SecondaryPhone?.Length > 0)
.WithMessage("SecondaryPhoneType must be set when a SecondaryPhone value is present.");
}
}
}
@@ -0,0 +1,16 @@
using FluentValidation;
using Landmark.OLA.Models.Individuals.Enums;
namespace Landmark.OLA.Models.Individuals.Validators
{
class EmployerValidator : AbstractValidator<Employer>
{
public EmployerValidator()
{
When(x => x.Income.Frequency == Frequency.Hourly, () =>
{
RuleFor(x => x.Income.HoursPerWeek).NotNull().WithMessage("Hourly employment must state hours per week.");
});
}
}
}