Browse Source

Save estimates to database from summary view

master
Immanuel Onyeka 1 year ago
parent
commit
af5c204eb6
6 changed files with 352 additions and 31 deletions
  1. +2
    -2
      components/app.vue
  2. +33
    -11
      components/new/details.vue
  3. +10
    -8
      components/new/new.vue
  4. +29
    -3
      components/new/summary.vue
  5. +2
    -1
      migrations/0_29092022_setup_tables.sql
  6. +276
    -6
      skouter.go

+ 2
- 2
components/app.vue View File

@@ -43,7 +43,6 @@ function getCookie(name) {

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

fetch(`/api/token`,
{method: 'GET',
@@ -54,6 +53,8 @@ function refreshToken() {
}).then(response => {
if (!response.ok) {
console.log("Error refreshing token.")
} else {
this.token = getCookie("skouter")
}
})

@@ -99,7 +100,6 @@ function getFees() {
}).then (result => {
if (!result || !result.length) return // Exit if token is invalid or no fees are saved
this.fees = result
console.log(result)
})

}


+ 33
- 11
components/new/details.vue View File

@@ -33,18 +33,39 @@

<section class="radios form">
<h3>Transaction Type</h3>
<input selected type="radio" name="transaction_type" value="0"
<input selected type="radio" name="transaction_type"
:selected="estimate.transaction == 'Purchase'"
:value="estimate.transaction"
@input="() => $emit('update:transaction', 0)"
@input="() => $emit('update:transaction', 'Purchase')"
>
<label>Purchase</label>
<input type="radio" name="transaction_type" value="1"
<input type="radio" name="transaction_type"
:selected="estimate.transaction == 'Refinance'"
:value="estimate.transaction"
@input="() => $emit('update:transaction', 1)"
@input="() => $emit('update:transaction', 'Refinance')"
>
<label>Refinance</label>
</section>

<section class="radios form">
<h3>Occupancy</h3>
<input type="radio" name="occupancy"
:value="estimate.occupancy"
@input="() => $emit('update:occupancy', 'Primary')"
>
<label>Primary</label>
<input type="radio" name="occupancy"
:value="estimate.occupancy"
@input="() => $emit('update:occupancy', 'Secondary')"
>
<label>Secondary</label>
<input type="radio" name="occupancy"
:value="estimate.occupancy"
@input="() => $emit('update:occupancy', 'Residential')"
>
<label>Residential</label>
</section>

<section class="form inputs">
<h3>Property Details</h3>
<label>Price ($)</label>
@@ -53,10 +74,10 @@
<select id="" name=""
:value="estimate.property"
@change="(e) => $emit('update:property', e.target.value)">
<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>
<option value="Single Attached">Single Family Attached</option>
<option value="Single Detached">Single Family Detached</option>
<option value="Condo Lo-rise">Lo-rise (4 stories or less)</option>
<option value="Condo Hi-rise">Hi-rise (over 4 stories)</option>
</select>
</section>

@@ -113,8 +134,8 @@
<label>Total DTI (%) - Optional</label>
<input :value="loan.dti" @input="(e) => $emit('update:dti', e)">
<label>Home Owner's Association ($/month)</label>
<input :value="loan.hoa / 100"
@input="(e) => { $emit('update:hoa', strip(e)) }">
<input :value="loan.hoi / 100"
@input="(e) => { $emit('update:hoi', strip(e)) }">

<label>Interest Rate (%)</label>
<input :value="loan.interest"
@@ -256,6 +277,7 @@ export default {
'update:borrowerCredit',
'update:borrowerIncome',
'update:transaction',
'update:occupancy',
'update:price',
'update:property',
@@ -267,7 +289,7 @@ export default {
'update:amount',
'update:housingDti',
'update:dti',
'update:hoa',
'update:hoi',
'update:interest',
'update:interestDays',
'update:hazardEscrow',


+ 10
- 8
components/new/new.vue View File

@@ -27,29 +27,30 @@ 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"
:token="token"
@update:name="(name) => loans[sel].title = name"
@update:name="(name) => loan.title = name"
@del="del"
@update:borrowerNum="(b) => estimate.borrower.num = b"
@update:borrowerCredit="(c) => estimate.borrower.credit = c"
@update:borrowerIncome="(m) => estimate.borrower.income = m*100"
@update:borrowerIncome="(m) => estimate.borrower.income = Math.round(m*100)"
@update:transaction="(t) => estimate.transaction = t"
@update:occupancy="(t) => estimate.occupancy = t"
@update:price="setPrice"
@update:property="(p) => estimate.property = p"

@update:loanType="changeLoanType"
@update:term="(lt) => loan.term = lt"
@update:program="(p) => loans[sel].program = p"
@update:program="(p) => loan.program = p"
@update:ltv="setLtv"
@update:amount="setAmount"
@update:housingDti="setHousingDti"
@update:dti="setDti"
@update:hoa="(hoa) => loan.hoa = hoa*100"
@update:hoi="(hoi) => loan.hoi = Math.round(hoi*100)"
@update:interest="(i) => loan.interest = i"
@update:interestDays="(d) => loans[sel].interestDays = d"
@update:hazardEscrow="(h) => loans[sel].hazardEscrow = h"
@update:hazard="(h) => loan.hazard = h * 100"
@update:hazard="(h) => loan.hazard = Math.round(h * 100)"
@update:taxEscrow="(t) => loans[sel].taxEscrow = t"
@update:tax="(t) => loan.tax = t*100"
@update:tax="(t) => loan.tax = Math.round(t*100)"
@update:manualMI="perc => loan.mi.rate = perc"
@toggle:manualMIMonthly=
"() => loans[sel].mi.monthly = !loans[sel].mi.monthly"
@@ -81,7 +82,7 @@ const example = {
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: 10000, // Home owner's association monthly fee
hoi: 10000, // Home owner's association monthly fee
program: "",
pud: true, // Property under development
zip: '',
@@ -97,7 +98,8 @@ const loans = [
// Default estimate fields
const estimate = {
property: "",
transaction: 0,
transaction: "",
occupancy: "",
price: 0,
borrower: {num: 0, credit: 0, income: 0},
loans: loans,


+ 29
- 3
components/new/summary.vue View File

@@ -8,7 +8,10 @@
Mortgage insurance: ${{format(loan.amount*loan.mi.rate/100/12)}}
</label>
<label>Property taxes: ${{format(loan.tax)}}</label>
<label>Homeowner's Insurance: ${{format(loan.hoa)}}</label>
<label>Homeowner's Insurance: ${{format(loan.hoi)}}</label>
<label v-if="loan.hazard">
Hazard insurance: ${{format(loan.hazard)}}
</label>
</section>

<section class="form inputs">
@@ -22,7 +25,7 @@
</section>

<section class="form inputs">
<button>Save Estimate</button>
<button @click="create">Save Estimate</button>
<button>Generate PDF</button>
</section>

@@ -32,6 +35,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
let valid = ref(false)
let saved = ref(true)
const props = defineProps(['downpayment', 'loan', 'token', 'estimate'])

function amortize(principle, rate, periods) {
@@ -49,7 +53,7 @@ const loanPayment = computed(() => {
const totalMonthly = computed (() => {
let total = loanPayment.value +
props.loan.tax +
props.loan.hoa +
props.loan.hoi +
props.loan.hazard
if (props.loan.mi.monthly) {
@@ -102,6 +106,28 @@ function validate() {

}

function create() {
fetch(`/api/estimate`,
{method: 'POST',
body: JSON.stringify(props.estimate),
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${props.token}`,
},
}).then(resp => {
if (resp.ok && resp.status == 200) {
console.log('saved')
saved.value = true
return
} else {
// resp.text().then(t => this.errors = [t])
// window.location.hash = 'new'
resp.text().then(t => console.log(t))
}
})

}

// Print number of cents as a nice string of dollars
function format(num) {
return (num/100).toFixed(2)


+ 2
- 1
migrations/0_29092022_setup_tables.sql View File

@@ -86,10 +86,11 @@ CREATE TABLE loan (
type_id INT NOT NULL,
amount INT NOT NULL,
term INT, /* In years */
interest INT, /* Per year, precise to 2 decimals */
interest FLOAT(5, 2) DEFAULT 0,
ltv FLOAT(5, 2) DEFAULT 0,
dti FLOAT(5, 2) DEFAULT 1,
hoi INT DEFAULT 0, /* Hazard insurance annual payments */
tax INT DEFAULT 0, /* Real estate taxes */
name VARCHAR(30) DEFAULT '',
PRIMARY KEY (`id`),
FOREIGN KEY (estimate_id) REFERENCES estimate(id),


+ 276
- 6
skouter.go View File

@@ -94,10 +94,11 @@ type Loan struct {
Ltv float32 `json:"ltv"`
Dti float32 `json:"dti"`
Hoi int `json:"hoi"`
Tax int `json:"hoi"`
Interest float32 `json:"interest"`
Mi MI `json:"mi"`
Fees []Fee `json:"fees"`
Name string `json:"name"`
Name string `json:"title"`
}

type MI struct {
@@ -130,7 +131,7 @@ type Estimate struct {
Id int `json:"id"`
User int `json:"user"`
Borrower Borrower `json:"borrower"`
Transaction int `json:"transaction"`
Transaction string `json:"transaction"`
Price int `json:"price"`
Property string `json:"property"`
Occupancy string `json:"occupancy"`
@@ -242,7 +243,6 @@ func getLoanType(
loans = append(loans, loan)
}

log.Printf("The loans: %v", loans)
return loans, nil
}

@@ -499,9 +499,9 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {
"Condo Hi-rise": "CON",
}

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

body, err := json.Marshal(map[string]any{
@@ -877,11 +877,281 @@ func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
json.NewEncoder(w).Encode(user)
}

func queryBorrower(db *sql.DB, id int) ( Borrower, error ) {
var borrower Borrower
var query string
var err error

query = `SELECT
l.id,
l.credit_score,
l.num,
l.monthly_income
FROM borrower l WHERE l.id = ?
`
row := db.QueryRow(query, id)
err = row.Scan(
borrower.Id,
borrower.Credit,
borrower.Num,
borrower.Income,
)

if err != nil {
return borrower, err
}

return borrower, nil
}

// Must have an estimate ID 'e', but not necessarily a loan id 'id'
func queryLoan(db *sql.DB, e int, id int) ( []Loan, error ) {
var loans []Loan
var query string
var rows *sql.Rows
var err error
fmt.Println(e, id)

query = `SELECT
l.id,
l.estimate_id,
l.amount,
l.term,
l.interest,
l.ltv,
l.dti,
l.hoi,
l.tax,
l.name
FROM loan l WHERE l.id = CASE @e := ? WHEN 0 THEN l.id ELSE @e END AND
l.estimate_id = ?
`
rows, err = db.Query(query, id, e)

if err != nil {
return loans, err
}

defer rows.Close()

for rows.Next() {
var loan Loan

if err := rows.Scan(
&loan.Id,
&loan.EstimateId,
&loan.Amount,
&loan.Term,
&loan.Interest,
&loan.Ltv,
&loan.Dti,
&loan.Hoi,
&loan.Tax,
&loan.Name,
)
err != nil {
return loans, err
}
loans = append(loans, loan)
}

// Prevents runtime panics
if len(loans) == 0 { return loans, errors.New("Loan not found.") }

return loans, nil
}

func queryEstimate(db *sql.DB, id int) ( []Estimate, error ) {
var estimates []Estimate
var query string
var rows *sql.Rows
var err error

query = `SELECT
u.id,
u.user_id,
u.borrower_id,
u.transaction,
u.price,
u.property,
u.occupancy,
u.zip,
u.pud
FROM estimate u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END
`
rows, err = db.Query(query, id)


if err != nil {
return estimates, err
}

defer rows.Close()

for rows.Next() {
var estimate Estimate

if err := rows.Scan(
&estimate.Id,
&estimate.User,
&estimate.Borrower.Id,
&estimate.Transaction,
&estimate.Price,
&estimate.Property,
&estimate.Occupancy,
&estimate.Zip,
&estimate.Pud,
)
err != nil {
return estimates, err
}
estimates = append(estimates, estimate)
}

// Prevents runtime panics
if len(estimates) == 0 { return estimates, errors.New("Estimate not found.") }
for _, e := range estimates {
fmt.Println("here's the estimate ID:", e.Id)
e.Loans, err = queryLoan(db, e.Id, 0)
if err != nil { return estimates, err }
}
return estimates, nil
}

// Accepts a borrower struct and returns the id of the inserted borrower and
// any related error.
func insertBorrower(db *sql.DB, borrower Borrower) (int, error) {
var query string
var row *sql.Row
var err error
var id int // Inserted loan's id

query = `INSERT INTO borrower
(
credit_score,
monthly_income,
num
)
VALUES (?, ?, ?)
RETURNING id
`
row = db.QueryRow(query,
borrower.Credit,
borrower.Income,
borrower.Num,
)

err = row.Scan(&id)
if err != nil { return 0, err }

return id, nil
}

func insertLoan(db *sql.DB, loan Loan) (Loan, error){
var query string
var row *sql.Row
var err error
var id int // Inserted loan's id

query = `INSERT INTO loan
(
estimate_id,
type_id,
amount,
term,
interest,
ltv,
dti,
hoi,
tax,
name
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id
`
row = db.QueryRow(query,
loan.EstimateId,
loan.Type.Id,
loan.Amount,
loan.Term,
loan.Interest,
loan.Ltv,
loan.Dti,
loan.Hoi,
loan.Tax,
loan.Name,
)

err = row.Scan(&id)
if err != nil { return Loan{}, err }

loans, err := queryLoan(db, id, 0)
if err != nil { return Loan{}, err }

return loans[0], nil
}


func insertEstimate(db *sql.DB, estimate Estimate) (Estimate, error){
var query string
var row *sql.Row
var err error
// var id int // Inserted estimate's id
estimate.Borrower.Id, err = insertBorrower(db, estimate.Borrower)
if err != nil { return Estimate{}, err }

query = `INSERT INTO estimate
(
user_id,
borrower_id,
transaction,
price,
property,
occupancy,
zip,
pud
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id
`
row = db.QueryRow(query,
estimate.User,
estimate.Borrower.Id,
estimate.Transaction,
estimate.Price,
estimate.Property,
estimate.Occupancy,
estimate.Zip,
estimate.Pud,
)

err = row.Scan(&estimate.Id)
if err != nil { return Estimate{}, err }

for _, l := range estimate.Loans {
l.EstimateId = estimate.Id
_, err = insertLoan(db, l)
if err != nil { return estimate, err }
}
estimates, err := queryEstimate(db, estimate.Id)
if err != nil { return Estimate{}, err }

return estimates[0], nil
}

func createEstimate(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 }

estimate, err = insertEstimate(db, estimate)
if err != nil { http.Error(w, err.Error(), 422); return }
json.NewEncoder(w).Encode(estimate)
}



Loading…
Cancel
Save