Browse Source

Calculate new estimate results server side

master
Immanuel Onyeka 1 year ago
parent
commit
7ce7c9300e
5 changed files with 59 additions and 467 deletions
  1. +1
    -21
      components/estimates.vue
  2. +0
    -386
      components/new.vue
  3. +3
    -2
      components/new/new.vue
  4. +53
    -56
      components/new/summary.vue
  5. +2
    -2
      skouter.go

+ 1
- 21
components/estimates.vue View File

@@ -54,7 +54,7 @@ ${{(estimate.price / 100).toLocaleString()}}
<label>Total monthly: ${{format(l.result.totalMonthly)}}</label>
<label>Cash to close: ${{format(l.result.cashToClose)}}</label>
</div>
<button @click="() => download(estimate)">PDF</button>
<button @click="() => download(estimate)">Generate PDF</button>
<button @click="() => estimate = null">Cancel</button>
</div>

@@ -142,26 +142,6 @@ function getEstimates() {
})
}

function summarize() {
fetch(`/api/estimate/summarize`,
{method: 'POST',
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${props.token}`,
},
body: JSON.stringify(estimate.value),
}).then(response => {
if (response.ok) { return response.json() } else {
response.text().then(t => console.log(t))
}
}).then(result => {
console.log(result)
if (!result || !result.length) return // Exit if token is invalid or no fees are saved
console.log('done', result)
})

}

function download(estimate) {
fetch(`/api/pdf`,
{method: 'POST',


+ 0
- 386
components/new.vue View File

@@ -1,386 +0,0 @@
<template>
<div id="new" class="page">

<h2>New Loan</h2>

<section class="loans-list">

<h3 v-for="(l, indx) in loans"
:class="sel == indx ? 'sel' : ''"
@click="() => sel = indx"
>
{{l.title}}
</h3>

<div class="add">
<svg @click="create"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0
0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/> </svg>
</div>
</section>

<section class="form inputs">
<h3>Loan</h3>
<label>Name</label>
<input :value="loans[sel].title" required
@input="(e) => loans[sel].title = stripLetters(e)">
<button @click="del">Delete</button>
</section>

<section class="form inputs">

<div class="hint">
<img class="icon" src="/assets/image/icon/question-circle.svg" alt="">
<div class="tooltip">
<p>Assumes borrower is not self employed, not bankrupt in the past 7
years, a citizen, and intends to occupy the property.</p>
</div>
</div>

<h3>Borrower</h3>
<label>Number of Borrowers</label>
<input :value="estimate.borrowers"
@input="(e) => estimate.borrowers = stripInt(e)">
<label>Credit Score</label>
<input :value="estimate.creditScore"
@input="(e) => estimate.creditScore = stripInt(e)">
<label>Monthly Income ($)</label>
<input :value="estimate.mIncome"
@input="(e) => estimate.mIncome = strip(e)">

</section>

<section class="radios form">
<h3>Transaction Type</h3>
<input selected type="radio" name="transaction_type" value="0"
v-model="estimate.transaction" >
<label>Purchase</label>
<input type="radio" name="transaction_type" value="1"
v-model="estimate.transaction">
<label>Refinance</label>
</section>

<section class="form inputs">
<h3>Property Details</h3>
<label>Price ($)</label>
<input :value="estimate.price" @input="setPrice">
<label>Type</label>
<select id="" name="" v-model="estimate.property">
<option value="attched">Single Family Attached</option>
<option value="detached">Single Family Detached</option>
<option value="lorise">Lo-rise (4 stories or less)</option>
<option value="hirise">Hi-rise (over 4 stories)</option>
</select>
</section>

<section class="radios form">
<h3>Loan Type</h3>
<input type="radio" name="loan_type" value="conv" v-model="loans[sel].type"
>
<label>Conventional</label>
<input type="radio" name="loan_type" value="fha" v-model="loans[sel].type">
<label>FHA</label>
<input type="radio" name="loan_type" value="va" v-model="loans[sel].type">
<label>VA</label>
<input type="radio" name="loan_type" value="usda" v-model="loans[sel].type">
<label>USDA</label>
</section>

<section class="form inputs">
<h3>Loan Details</h3>
<label>Loan Term (years)</label>
<input :value="loans[sel].term"
@input="(e) => loans[sel].term = strip(e)">

<label>Loan Program</label>
<select id="" name="" v-model="loans[sel].program">
<option value="none">None</option>
</select>

<label>Loan to Value (%)</label>
<input :value="loans[sel].ltv" @input="setLtv">
<label>Loan Amount ($)</label>
<input :value="loans[sel].amount"
@input="setAmount">
<label>Housing Expense DTI (%) - Optional</label>
<input :value="loans[sel].housingDti" @input="setHousingDti">
<label>Total DTI (%) - Optional</label>
<input :value="loans[sel].dti" @input="setDti">
<label>Home Owner's Association ($/month)</label>
<input :value="loans[sel].hoa"
@input="(e) => { loans[sel].hoa = strip(e) }">

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

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

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

<section class="form inputs">
<h3>Fees</h3>
<div v-for="(fee, indx) in estimate.loans[sel].fees"
:key="fee.name + indx" class="fee"
>
<label>
${{fee.amount}}{{ fee.perc ? ` ${fee.perc}%` : ''}} - {{fee.name}}<br>
{{fee.type}}
</label>
<img width="21" height="21" src="/assets/image/icon/x-red.svg"
@click="() => estimate.loans[sel].fees.splice(indx, 1)">
</div>
<button @click="resetFees">Reset</button>
<button @click="createFee">New</button>
</section>

<fee-dialog v-if="newFee"
:heading="'New Fee'"
:initial="{}"
:price="estimate.price"
@close="() => newFee = null"
@save="addFee"
/>

<section class="form radios">
<h3>Mortgage Insurance</h3>
<input type="radio" name="transaction_type" value="transaction"
@input="e => estimate.transaction = 0"
selected="estimate.transaction == 0">
<label>1.43% - National MI</label>
<input type="radio" name="transaction_type" value="refinance"
@input="e => estimate.transaction = 1"
selected="estimate.transaction == 1">
<label>0.73% - MGIC</label>
</section>

<section class="form inputs">

<button @click="generate">Generate</button>

<div class="errors">
<span v-for="e in errors">{{e}}</span>
</div>

</section>

</div>
</template>

<script>
import Dialog from "./dialog.vue"
import FeeDialog from "./fee-dialog.vue"
import { stripLetters, strip, stripInt, stripPerc } from "../helpers.js"

// The default values of a new estimate
const example = {
title: "Example",
type: "",
term: 0,
ltv: 0, // Loan to home value ratio
dti: 0,
housingDti: 0,
amount: 0,
interest: 0,
interestDays: 0,
hazard: 0, // Hazard insurance monthly payment
hazardEscrow: 0, // Hazard insurance escrow in months (0 is none)
tax: 0, // Real Estate taxes monthly payment
taxEscrow: 0, // Months to escrow (0 is none)
hoa: 100, // Home owner's association monthly fee
program: "",
pud: true, // Property under development
zip: '',
fees: [],
}

// The default loans on a new estimate
const loans = [
Object.assign({}, example,),
Object.assign(
Object.assign({}, example),
{title: "Another One",}
),
]

// Default estimate fields
const estimate = {
property: "",
transaction: 0,
price: 0,
borrowers: 0,
creditScore: 0,
mIncome: 0,
loans: loans,
}

const newFee = {
name: '', type: '', amount: 0, perc: 0
}

// Clone loan from initial example as a new loan
function create() {
this.estimate.loans.push(
Object.assign({}, example, {fees: this.createFees()})
)
}

function createFees() {
return this.fees.map(f => Object.assign({}, f))
}

// Setup this.newFee for the creation dialog
function createFee() {
this.newFee = Object.assign({}, newFee)
}

function resetFees() {
this.estimate.loans[this.sel].fees = this.createFees()
}

// If valid, add the current this.newFee to the array of fees and reset
// this.newFee to null
function addFee(fee, isDebit) {

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

function del() {
if (this.loans.length > 1) {
let x = this.sel
this.sel = 0
this.loans.splice(x, 1)
}
}

// Changes loan.ltv's <input> and data() values, then syncs with data.amount
function setLtv(e) {
let ltv = strip(e)
let loan = this.loans[this.sel]
if (!this.estimate.price) return

if (ltv > 100) ltv = 100
if (ltv < 0) ltv = 0

loan.ltv = ltv
loan.amount = (ltv / 100 * this.estimate.price).toFixed(2)
}

// Changes loan.amount's <input> and data() values, then syncs with data.ltv
function setAmount(e) {
let amount = strip(e)
let loan = this.loans[this.sel]
if (!this.estimate.price) return

if (amount > loan.price) amount = loan.price
if (amount < 0) amount = 0

loan.amount = amount
loan.ltv = (amount / this.estimate.price * 100).toFixed(2)
}

// Updates the property price for all loans and their fee amounts.
function setPrice(e) {
let value = strip(e)
this.estimate.price = value
this.estimate.loans[this.sel].fees.forEach(fee => {
if (fee.perc) fee.amount = (fee.perc / 100 * value).toFixed(2)
})
}

function setDti(e) {
let dti = strip(e)
let loan = this.loans[this.sel]
if (!loan.price) return

if (dti > 100) dti = 100
if (dti < 0) dti = 0

e.target.value = dti
loan.dti = dti
}

function setHousingDti(e) {
let housingDti = strip(e)
let loan = this.loans[this.sel]
if (!loan.price) return

if (housingDti > 100) housingDti = 100
if (housingDti < 0) housingDti = 0

e.target.value = housingDti
loan.housingDti = housingDti
}

function generate() {
this.errors = this.validate()
}

function validate() {
let errors = []
const estimate = this.estimate

// Alternative attribute names for error messages
const names = {
term: "loan term",
ltv: "loan to value",
hazard: "hazard insurance",
hazardEscrow: "hazard insurance escrow",
}

if (!estimate.property) {
errors.push("Missing property type.")
} else if (!estimate.price) {
errors.push("Missing property price.")
} else if (!estimate.borrowers) {
errors.push("Missing number of borrowers.")
} else if (!estimate.creditScore) {
errors.push("Missing credit score.")
} else if (!estimate.mIncome) {
errors.push("Missing monthly income.")
}

return errors
}

// Percentage values of fees always takek precedent over amounts. The conversion
// happens in setPrice()
export default {
components: { Dialog, FeeDialog },
methods: {
setPrice, setLtv, setAmount, setDti, setHousingDti, strip, stripInt,
stripLetters, del, create, createFees, createFee, resetFees,
addFee, generate, validate
},
props: ['user', 'fees'],
data() {
return {
estimate: estimate,
loans: estimate.loans,
sel: 0,
newFee: null,
errors: [],
}
},
created() {
this.estimate.loans.forEach(l => l.fees = this.createFees())
}
}
</script>

+ 3
- 2
components/new/new.vue View File

@@ -60,7 +60,8 @@ class="bi bi-plus" viewBox="0 0 16 16"> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0
:loan="loan"
:downpayment="estimate.price - loan.amount"
:token="token"
:estimate="estimate"/>
:estimate="estimate"
@update="(e) => estimate = e" />

</div>
</template>
@@ -99,7 +100,7 @@ const loans = [
]

// Default estimate fields
const estimate = {
let estimate = {
property: "",
transaction: "",
occupancy: "",


+ 53
- 56
components/new/summary.vue View File

@@ -1,9 +1,9 @@
<template>
<div v-if="valid">
<div v-if="valid && props.loan.result">

<section class="form inputs">
<h3>Monthly Payment - ${{format(totalMonthly)}}</h3>
<label>Loan payment: ${{format(loanPayment)}}</label>
<h3>Monthly Payment - ${{format(loan.result.totalMonthly)}}</h3>
<label>Loan payment: ${{format(loan.result.loanPayment)}}</label>
<label v-if="loan.mi.monthly">
Mortgage insurance: ${{format(loan.amount*loan.mi.rate/100/12)}}
</label>
@@ -15,9 +15,11 @@
</section>

<section class="form inputs">
<h3>Cash to Close - ${{format(cashToClose)}}</h3>
<label>Closing costs: ${{format(fees)}}</label>
<label v-if="credits">Credits: ${{credits}}</label>
<h3>Cash to Close - ${{format(loan.result.cashToClose)}}</h3>
<label>Closing costs: ${{format(loan.result.totalFees)}}</label>
<label v-if="loan.result.totalCredits">
Credits: ${{format(loan.result.totalCredits)}}
</label>
<label>Down payment: ${{format(downpayment)}}</label>
<label v-if="!loan.mi.monthly">
Mortgage insurance: ${{format(loan.amount*loan.mi.rate/100)}}
@@ -26,65 +28,31 @@

<section class="form inputs">
<button :disabled="saved" @click="create">Save Estimate</button>
<button>Generate PDF</button>
<button @click="() => download(props.estimate)">Generate PDF</button>
</section>

<DDialog v-if="dlink" @close="() => dlink = ''"
:fileName="`estimate-${props.estimate.id}.pdf`" :url="dlink">
</DDialog>

</div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { format } from '../../helpers.js'
import DDialog from "../download-dialog.vue"

const props = defineProps(['downpayment', 'loan', 'token', 'estimate'])
const emit = defineEmits(['update'])
let valid = ref(false)
let saved = ref(false)
const props = defineProps(['downpayment', 'loan', 'token', 'estimate'])
let dlink = ref('')

function amortize(principle, rate, periods) {
return principle * rate*(1+rate)**periods / ((1+rate)**periods - 1)
}

const loanPayment = computed(() => {
return amortize(props.loan.amount,
props.loan.interest / 100 / 12,
props.loan.term*12)
})
const totalMonthly = computed (() => {
let total = loanPayment.value +
props.loan.tax +
props.loan.hoi +
props.loan.hazard
if (props.loan.mi.monthly) {
total = total + props.loan.mi.rate/100*(props.loan.amount)
}
return total
})

// Closing costs
const fees = computed(() => {
return props.loan.fees.reduce((total, x) => {
return x.amount > 0 ? total + x.amount : 0
}, 0
)
})

const credits = computed(() => {
return props.loan.fees.reduce((total, x) => {
return x.amount < 0 ? total + x.amount : 0
}, 0
)
})

const cashToClose = computed(() => {
let total = fees.value + credits.value + props.downpayment
if (!props.loan.mi.monthly) {
total = total + props.loan.mi.rate/100*(props.loan.amount)
}
return total
})

function validate() {
fetch(`/api/estimate/validate`,
{method: 'POST',
@@ -95,12 +63,25 @@ function validate() {
},
}).then(resp => {
if (resp.ok && resp.status == 200) {
valid.value = true
return
} else {
// resp.text().then(t => this.errors = [t])
// window.location.hash = 'new'
}
valid.value = true
summarize()
}
})
}

function summarize() {
fetch(`/api/estimate/summarize`,
{method: 'POST',
body: JSON.stringify(props.estimate),
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${props.token}`,
},
}).then(resp => {
if (resp.ok && resp.status == 200) return resp.json()
}).then(e => {
console.log(e)
emit('update', e)
})

}
@@ -128,6 +109,22 @@ function create() {

}

function download(estimate) {
fetch(`/api/pdf`,
{method: 'POST',
body: JSON.stringify(estimate),
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${props.token}`,
},
}).then(response => {
if (response.ok) { return response.blob() }
}).then (result => {
if (!result) return // Exit if token is invalid or blank file returned
dlink.value = URL.createObjectURL(result)
})
}

onMounted(() => {validate()})

</script>

+ 2
- 2
skouter.go View File

@@ -366,9 +366,9 @@ func summarize(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 estimate.", 422); return }
results := estimate.makeResults()
estimate.makeResults()
json.NewEncoder(w).Encode(results)
json.NewEncoder(w).Encode(estimate)
}

func getLoanType( db *sql.DB, id int) (LoanType, error) {


Loading…
Cancel
Save