Created
August 31, 2023 16:49
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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