Skip to content

Instantly share code, notes, and snippets.

@ijrussell
Created August 31, 2023 16:49
Show Gist options
  • Save ijrussell/dbdf99b8bca6b0a6186710e6498b8841 to your computer and use it in GitHub Desktop.
Save ijrussell/dbdf99b8bca6b0a6186710e6498b8841 to your computer and use it in GitHub Desktop.
F# version of the core processing part of https://github.com/ijrussell/LendingPlatform
#r "nuget: FsToolkit.ErrorHandling"
open System
open FsToolkit.ErrorHandling
type LoanAmount = LoanAmount of int
type AssetValue = AssetValue of int
type CreditScore = CreditScore of int16
type LoanToValueRate = LoanToValueRate of byte
type LoanApplicationRequest = {
LoanAmount: option<int>
AssetValue: option<int>
CreditScore: option<int16>
}
type ValidatedLoanApplicationRequest = {
LoanAmount: LoanAmount
AssetValue: AssetValue
CreditScore: CreditScore
}
type LoanApplicationStatus =
| Approved
| Declined of Reasons:list<string>
type ProcessedLoanApplication = {
LoanAmount: LoanAmount
AssetValue: AssetValue
CreditScore: CreditScore
LoanToValueRate: LoanToValueRate
Status: LoanApplicationStatus
}
type LoanApplicationResponse =
| Processed of ProcessedLoanApplication
| UnableToProcess of Reasons:list<string>
type RiskDecider = LoanAmount -> LoanToValueRate -> CreditScore -> LoanApplicationStatus
[<RequireQualifiedAccess>]
module LoanApplicationResponse =
let private createProcessed (request:ValidatedLoanApplicationRequest) loanToValueRate status =
{
LoanAmount = request.LoanAmount
AssetValue = request.AssetValue
CreditScore = request.CreditScore
LoanToValueRate = loanToValueRate
Status = status
}
|> Processed
let isApproved request loanToValueRate =
LoanApplicationStatus.Approved
|> createProcessed request loanToValueRate
let isDeclined request loanToValueRate reasons =
LoanApplicationStatus.Declined reasons
|> createProcessed request loanToValueRate
[<RequireQualifiedAccessAttribute>]
module LoanApplicationRequest =
let private validateLoanAmount input =
match input with
| Some amount when amount > 0 -> amount |> LoanAmount |> Ok
| _ -> Error ["LoanAmount is invalid"]
let private validateAssetValue input =
match input with
| Some value when value > 0 -> value |> AssetValue |> Ok
| _ -> Error ["AssetValue is invalid"]
let private validateCreditScore input =
match input with
| Some score when score >= 1s && score <= 999s -> score |> CreditScore |> Ok
| _ -> Error ["CreditScore is invalid"]
let validate (request:LoanApplicationRequest) : Result<ValidatedLoanApplicationRequest,list<string>> =
validation {
let! loanAmount = request.LoanAmount |> validateLoanAmount
and! assetValue = request.AssetValue |> validateAssetValue
and! creditScore = request.CreditScore |> validateCreditScore
return { LoanAmount = loanAmount; AssetValue = assetValue; CreditScore = creditScore }
}
[<RequireQualifiedAccess>]
module LoanToValueRate =
let calculate (LoanAmount loanAmount) (AssetValue assetValue) =
if assetValue = 0 || loanAmount >= assetValue then None
else Math.Floor(100m * decimal loanAmount / decimal assetValue) |> byte |> LoanToValueRate |> Some
[<RequireQualifiedAccess>]
module RiskDecider =
let private isAcceptableRisk loanAmount loanToValueRate creditScore =
match loanAmount, loanToValueRate with
| amount, rate when amount >= 1_000_000 -> (rate <= 60uy && creditScore >= 950s)
| _, rate when rate < 60uy -> creditScore >= 750s
| _, rate when rate < 80uy -> creditScore >= 800s
| _, rate when rate < 90uy -> creditScore >= 900s
| _ -> false
let private (|LoanAmountIsNotAllowed|_|) (LoanAmount loanAmount) =
if loanAmount < 100_000 || loanAmount > 1_500_000 then Some ()
else None
let private (|UnacceptableRisk|_|) (LoanAmount loanAmount, LoanToValueRate loanToValueRate, CreditScore creditScore) =
isAcceptableRisk loanAmount loanToValueRate creditScore
|> fun isAcceptable ->
if not isAcceptable then Some () else None
let run : RiskDecider =
fun loanAmount loanToValueRate creditScore ->
match loanAmount, loanToValueRate, creditScore with
| LoanAmountIsNotAllowed, _, _ ->
Declined ["Loan amount requested is not allowed."]
| UnacceptableRisk ->
Declined ["Unacceptable risk for the amount that you wish to borrow."]
| _ ->
Approved
let processLoanApplication (riskDecider:RiskDecider) (request:LoanApplicationRequest) : LoanApplicationResponse =
result {
let! validated = LoanApplicationRequest.validate request
let! loanToValueRate =
LoanToValueRate.calculate validated.LoanAmount validated.AssetValue
|> Result.requireSome ["Unable to calculate loan to value rate."]
return
match riskDecider validated.LoanAmount loanToValueRate validated.CreditScore with
| Approved -> LoanApplicationResponse.isApproved validated loanToValueRate
| Declined reasons -> LoanApplicationResponse.isDeclined validated loanToValueRate reasons
} // Result<LoanApplicationResponse,list<string>>
|> function
| Ok processed -> processed
| Error reason -> LoanApplicationResponse.UnableToProcess reason
let processWithRiskDecider = processLoanApplication RiskDecider.run
// Simple set of asserts
let invalidLoanAmount =
processWithRiskDecider { LoanAmount = None; AssetValue = Some 600_000; CreditScore = Some 900s } =
UnableToProcess ["LoanAmount is invalid"]
let invalidAssetValue =
processWithRiskDecider { LoanAmount = Some 600_000; AssetValue = None; CreditScore = Some 900s } =
UnableToProcess ["AssetValue is invalid"]
let invalidCreditScore =
processWithRiskDecider { LoanAmount = Some 600_000; AssetValue = Some 800_000; CreditScore = None } =
UnableToProcess ["CreditScore is invalid"]
let declined =
processWithRiskDecider { LoanAmount = Some 200_000; AssetValue = Some 400_000; CreditScore = Some 500s } =
Processed {
LoanAmount = LoanAmount 200000
AssetValue = AssetValue 400000
CreditScore = CreditScore 500s
LoanToValueRate = LoanToValueRate 50uy
Status = Declined ["Unacceptable risk for the amount that you wish to borrow."]
}
let approved =
processWithRiskDecider { LoanAmount = Some 200_000; AssetValue = Some 300_000; CreditScore = Some 800s } =
Processed {
LoanAmount = LoanAmount 200000
AssetValue = AssetValue 300000
CreditScore = CreditScore 800s
LoanToValueRate = LoanToValueRate 66uy
Status = Approved
}
// Results of asserts
// val invalidLoanAmount: bool = true
// val invalidAssetValue: bool = true
// val invalidCreditScore: bool = true
// val declined: bool = true
// val approved: bool = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment