Procházet zdrojové kódy

Move form validation to golang backend

master
Immanuel Onyeka před 1 rokem
rodič
revize
0c18bfdab8
4 změnil soubory, kde provedl 150 přidání a 52 odebrání
  1. +4
    -15
      components/app.vue
  2. +37
    -23
      components/new/details.vue
  3. +6
    -4
      components/new/new.vue
  4. +103
    -10
      skouter.go

+ 4
- 15
components/app.vue Zobrazit soubor

@@ -11,7 +11,7 @@
</div>

<home :user="user" v-else-if="active == 1" />
<new-estimate :user="user" :fees="fees" v-else-if="active == 2" />
<new-estimate :user="user" :fees="fees" :token="token" v-else-if="active == 2" />
<estimates :user="user" :fees="fees" v-else-if="active == 3" />
<settings :user="user" v-else-if="active == 4" />
<sign-out :user="user" v-else-if="active == 5" />
@@ -43,6 +43,7 @@ function getCookie(name) {

function refreshToken() {
const token = getCookie("skouter")
this.token = token

fetch(`/api/token`,
{method: 'GET',
@@ -62,6 +63,7 @@ function refreshToken() {

function getUser() {
const token = getCookie("skouter")
this.token = token

return fetch(`/api/user`,
{method: 'GET',
@@ -101,20 +103,6 @@ function getFees() {

}

// The default fees of a new loan. Percentage values take precedent over amounts
const fees = [
{ name: 'Processing fee', type: 'Lender Fees', amount: 500, perc: 0 },
{ name: 'Underwriting fee', type: 'Lender Fees', amount: 500, perc: 0 },
{ name: 'Credit Report', type: 'Services Required by Lender',
amount: 52.50 },
{ name: 'Appraisal', type: 'Services Required by Lender', amount: 52.50 },
{ name: 'Title Services', type: 'Title Company', amount: 1000 },
{ name: 'Lender\'s Title Insurance', type: 'Title Company', amount: 1599 },
{ name: 'Owner\'s Title Insurance', type: 'Title Company', amount: 451.00 },
{ name: 'Recording Charges', type: 'Government', amount: 99.00 },
{ name: 'State Tax', type: 'Government', amount: 2411.00 },
]

// Used to check the current section of the app generally without a regex match
// each time.
function active() {
@@ -183,6 +171,7 @@ export default {
hash: window.location.hash,
fees: [],
loadingError: "",
token: '',
}
},
created() {


+ 37
- 23
components/new/details.vue Zobrazit soubor

@@ -3,7 +3,7 @@
<section class="form inputs">
<h3>Loan</h3>
<label>Name</label>
<input :value="loans[sel].title" required
<input :value="loan.title" required
@input="(e) => $emit('update:name', stripLetters(e))">
<button @click="() => $emit('del')">Delete</button>
</section>
@@ -71,19 +71,19 @@
<input type="radio"
name="loan_type"
value="fha"
:checked="loans[sel].type == 'fha'"
:checked="loan.type == 'fha'"
@change="(e) => $emit('update:loanType', e.target.value)">
<label>FHA</label>
<input type="radio"
name="loan_type"
value="va"
:checked="loans[sel].type == 'va'"
:checked="loan.type == 'va'"
@change="(e) => $emit('update:loanType', e.target.value)">
<label>VA</label>
<input type="radio"
name="loan_type"
value="usda"
:checked="loans[sel].type == 'usda'"
:checked="loan.type == 'usda'"
@change="(e) => $emit('update:loanType', e.target.value)">
<label>USDA</label>
</section>
@@ -91,12 +91,12 @@
<section class="form inputs">
<h3>Loan Details</h3>
<label>Loan Term (years)</label>
<input :value="loans[sel].term"
<input :value="loan.term"
@input="(e) => $emit('update:term', stripInt(e))">

<label>Loan Program</label>
<select id="" name=""
:value="loans[sel].program"
:value="loan.program"
@change="(e) => $emit('update:program', e.target.value)">
<option value="none">None</option>
</select>
@@ -107,39 +107,39 @@
<input :value="loan.amount"
@input="(e) => $emit('update:amount', e)">
<label>Housing Expense DTI (%) - Optional</label>
<input :value="loans[sel].housingDti"
<input :value="loan.housingDti"
@input="(e) => $emit('update:housingDti', e)">
<label>Total DTI (%) - Optional</label>
<input :value="loans[sel].dti" @input="(e) => $emit('update:dti', e)">
<input :value="loan.dti" @input="(e) => $emit('update:dti', e)">
<label>Home Owner's Association ($/month)</label>
<input :value="loans[sel].hoa"
<input :value="loan.hoa"
@input="(e) => { $emit('update:hoa', strip(e)) }">

<label>Interest Rate (%)</label>
<input :value="loans[sel].interest"
<input :value="loan.interest"
@input="(e) => { $emit('update:interest', stripPerc(e)) }">
<label>Days of Interest</label>
<input :value="loans[sel].interestDays"
<input :value="loan.interestDays"
@input="(e) => $emit('update:interestDays', stripInt(e))">

<label>Hazard Insurance Escrow (months)</label>
<input :value="loans[sel].hazardEscrow"
<input :value="loan.hazardEscrow"
@input="(e) => { $emit('update:hazardEscrow', stripInt(e)) }">
<label>Hazard Insurance ($/month)</label>
<input :value="loans[sel].hazard"
<input :value="loan.hazard"
@input="(e) => $emit('update:hazard', strip(e))">

<label>Real Estate Tax Escrow (months)</label>
<input :value="loans[sel].taxEscrow"
<input :value="loan.taxEscrow"
@input="e => $emit('update:taxEscrow', stripInt(e))">
<label>Real Estate Tax ($/month)</label>
<input :value="loans[sel].tax"
<input :value="loan.tax"
@input="(e) => $emit('update:tax', strip(e))">
</section>

<section class="form inputs">
<h3>Fees</h3>
<div v-for="(fee, indx) in estimate.loans[sel].fees"
<div v-for="(fee, indx) in loan.fees"
:key="fee.name + indx" class="fee"
>
<label>
@@ -147,7 +147,7 @@
{{fee.type}}
</label>
<img width="21" height="21" src="/assets/image/icon/x-red.svg"
@click="() => estimate.loans[sel].fees.splice(indx, 1)">
@click="() => loan.fees.splice(indx, 1)">
</div>
<button @click="resetFees">Reset</button>
<button @click="createFee">New</button>
@@ -165,18 +165,18 @@
<h3>Mortgage Insurance</h3>
<div class="row">
<input checked type="radio" name="mi"/><label>Manual %</label>
<input type="checkbox" :checked="loans[sel].mi.monthly"
<input type="checkbox" :checked="loan.mi.monthly"
@change="e => $emit('toggle:manualMIMonthly')" />
<label>monthly</label>
</div>
<div class="row">
<input :value="loans[sel].mi.rate" @input="e => $emit('update:manualMI', stripPerc(e))" />
<input :value="loan.mi.rate" @input="e => $emit('update:manualMI', stripPerc(e))" />
</div>
</section>

<section class="form inputs">

<button @click="() => validate() && $emit('continue')">Continue</button>
<button @click="validate">Continue</button>

<ul class="errors">
<li v-for="e in errors">{{e}}</li>
@@ -205,12 +205,25 @@ function createFee() {
function addFee(fee, isDebit) {

if (!isDebit) fee.amount = fee.amount * -1
this.estimate.loans[this.sel].fees.push(fee)
this.loan.fees.push(fee)
this.newFee = null
}


function validate() {
fetch(`/api/estimate/validate`,
{method: 'POST',
body: JSON.stringify(this.estimate),
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${this.token}`,
},
}).then(response => {
if (response.ok) { return response.json() }
response.text().then(t => this.errors = [t])
}).then(result => {
if (result) this.$emit('continue', result)
})
/*
let errors = []
const estimate = this.estimate

@@ -252,6 +265,7 @@ function validate() {
}
return true
*/
}

function generate() {
@@ -268,7 +282,7 @@ export default {
strip, stripInt, stripLetters, stripPerc, createFee, addFee, validate,
generate
},
props: ['estimate', 'loans', 'sel', 'loan'],
props: ['estimate', 'loan', 'token'],
// Loan updates assume the currently selected loan is being modified, and
// $emit has no need to clarify.
emits: [


+ 6
- 4
components/new/new.vue Zobrazit soubor

@@ -25,6 +25,7 @@ class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0
:loans="estimate.loans"
:sel="sel"
:loan="loan"
:token="token"
@update:name="(name) => loans[sel].title = name"
@del="del"
@@ -85,7 +86,7 @@ const example = {
pud: true, // Property under development
zip: '',
fees: [],
mi: {monthly: false, rate: 0}
mi: { monthly: false, rate: 0 }
}

// The default loans on a new estimate
@@ -192,7 +193,7 @@ function setHousingDti(e) {
}

function generate() {
window.location.hash = 'new/summary'
window.location.hash = 'new/summary'
}
// Percentage values of fees always take precedent over amounts. The conversion
@@ -204,7 +205,7 @@ export default {
setDti, setHousingDti
},
computed: { loan },
props: ['user', 'fees'],
props: ['user', 'fees', 'token'],
data() {
return {
estimate: estimate,
@@ -216,7 +217,8 @@ export default {
},
created() {
this.estimate.loans.forEach(l => l.fees = this.createFees())
window.addEventListener("hashchange", () => this.hash = window.location.hash)
window.addEventListener("hashchange",
() => this.hash = window.location.hash)
}
}
</script>

+ 103
- 10
skouter.go Zobrazit soubor

@@ -130,7 +130,7 @@ type Estimate struct {
Id int `json:"id"`
User int `json:"user"`
Borrower Borrower `json:"borrower"`
Transaction string `json:"transaction"`
Transaction int `json:"transaction"`
Price int `json:"price"`
Property string `json:"property"`
Occupancy string `json:"occupancy"`
@@ -499,9 +499,9 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {
"Condo Hi-rise": "CON",
}

var purposeCodes = map[string]string {
"Purchase": "PUR",
"Refinance": "RRT",
var purposeCodes = map[int]string {
1: "PUR",
2: "RRT",
}

body, err := json.Marshal(map[string]any{
@@ -632,10 +632,6 @@ func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) {
http.StatusInternalServerError)}
}

func validateEstimate() {
return
}

func getClaims(r *http.Request) (UserClaims, error) {
claims := new(UserClaims)
_, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ")
@@ -886,11 +882,104 @@ func createEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
err := json.NewDecoder(r.Body).Decode(&estimate)
if err != nil { http.Error(w, "Invalid fields.", 422); return }

if err != nil { http.Error(w, "Error creating user.", 422); return }

json.NewEncoder(w).Encode(estimate)
}

func checkConventional(l Loan, b Borrower) error {
if b.Credit < 620 {
return errors.New("Credit score too low for conventional loan")
}
// Buyer needs to put down 20% to avoid mortgage insurance
if (l.Ltv > 80 && l.Mi.Rate == 0) {
return errors.New(fmt.Sprintln(
l.Name,
"down payment must be 20% to avoid insurance",
))
}
return nil
}

func checkFHA(l Loan, b Borrower) error {
if b.Credit < 500 {
return errors.New("Credit score too low for FHA loan")
}
if (l.Ltv > 96.5) {
return errors.New("FHA down payment must be at least 3.5%")
}
if (b.Credit < 580 && l.Ltv > 90) {
return errors.New("FHA down payment must be at least 10%")
}
// Debt-to-income must be below 45% if credit score is below 580.
if (b.Credit < 580 && l.Dti > 45) {
return errors.New(fmt.Sprintln(
l.Name, "debt to income is too high for credit score.",
))
}
return nil
}

// Should also check loan amount limit maybe with an API.
func checkEstimate(e Estimate) error {
if e.Property == "" { return errors.New("Empty property type") }
if e.Price == 0 { return errors.New("Empty property price") }
if e.Borrower.Num == 0 {
return errors.New("Missing number of borrowers")
}
if e.Borrower.Credit == 0 {
return errors.New("Missing borrower credit score")
}
if e.Borrower.Income == 0 {
return errors.New("Missing borrower credit income")
}
for _, l := range e.Loans {
if l.Amount == 0 {
return errors.New(fmt.Sprintln(l.Name, "loan amount cannot be zero"))
}
if l.Term == 0 {
return errors.New(fmt.Sprintln(l.Name, "loan term cannot be zero"))
}
if l.Interest == 0 {
return errors.New(fmt.Sprintln(l.Name, "loan interest cannot be zero"))
}
// Can be used to check rules for specific loan types
var err error
switch l.Type.Name {
case "Conventional":
err = checkConventional(l, e.Borrower)
case "FHA":
err = checkFHA(l, e.Borrower)
case "VA":
err = checkConventional(l, e.Borrower)
case "USDA":
err = checkConventional(l, e.Borrower)
}
if err != nil { return err }
}
println("done")
return nil
}

func validateEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var estimate Estimate
err := json.NewDecoder(r.Body).Decode(&estimate)

if err != nil { http.Error(w, "Invalid fields.", 422); return }
err = checkEstimate(estimate)
if err != nil { http.Error(w, err.Error(), 406); return }
}

func api(w http.ResponseWriter, r *http.Request) {
var args []string

@@ -946,6 +1035,10 @@ func api(w http.ResponseWriter, r *http.Request) {
r.Method == http.MethodPost &&
guard(r, 1):
createEstimate(w, db, r)
case match(p, "/api/estimate/validate", &args) &&
r.Method == http.MethodPost &&
guard(r, 1):
validateEstimate(w, db, r)
}

db.Close()


Načítá se…
Zrušit
Uložit