@@ -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,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', | |||
@@ -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, | |||
@@ -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) | |||
@@ -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), | |||
@@ -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) | |||
} | |||