package main import ( "net/http" "net/mail" "log" "sync" "regexp" "html/template" "database/sql" _ "github.com/go-sql-driver/mysql" "fmt" "encoding/json" "strconv" "bytes" "time" "errors" "strings" "math" // pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" "github.com/golang-jwt/jwt/v4" ) type User struct { Id int `json:"id"` Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` BranchId int `json:"branchId"` Status string `json:"status"` Country string `json:"country"` Title string `json:"title"` Verified bool `json:"verified"` Role string `json:"role"` Password string `json:"password,omitempty"` } type UserClaims struct { Id int `json:"id"` Role string `json:"role"` Exp string `json:"exp"` } type Page struct { tpl *template.Template Title string Name string } type Borrower struct { Id int `json:"id"` Credit int `json:"credit"` Income int `json:"income"` Num int `json:"num"` } type FeeTemplate struct { Id int `json:"id"` User int `json:"user"` Branch int `json:"branch"` Amount int `json:"amount"` Perc int `json:"perc"` Type string `json:"type"` Notes string `json:"notes"` Name string `json:"name"` Category string `json:"category"` Auto bool `json:"auto"` } type Fee struct { Id int `json:"id"` LoanId int `json:"loan_id"` Amount int `json:"amount"` Perc float32 `json:"perc"` Type string `json:"type"` Notes string `json:"notes"` Name string `json:"name"` Category string `json:"category"` } type LoanType struct { Id int `json:"id"` User int `json:"user"` Branch int `json:"branch"` Name string `json:"name"` } type Loan struct { Id int `json:"id"` EstimateId int `json:"estimateId"` Type LoanType `json:"type"` Amount int `json:"amount"` Amortization string `json:"amortization"` Term int `json:"term"` Ltv float32 `json:"ltv"` Dti float32 `json:"dti"` Hoi int `json:"hoi"` Hazard int `json:"hazard"` Tax int `json:"tax"` Interest float32 `json:"interest"` Mi MI `json:"mi"` Fees []Fee `json:"fees"` Name string `json:"title"` } type MI struct { Type string `json:"user"` Label string `json:"label"` Lender string `json:"lender"` Rate float32 `json:"rate"` Premium float32 `json:"premium"` Upfront float32 `json:"upfront"` Monthly bool `json:"monthly"` FiveYearTotal float32 `json:"fiveYearTotal"` InitialAllInPremium float32 `json:"initialAllInPremium"` InitialAllInRate float32 `json:"initialAllInRate"` InitialAmount float32 `json:"initialAmount"` } type Result struct { Id int `json:"id"` LoanId int `json:"loanId"` LoanPayment int `json:"loanPayment"` TotalMonthly int `json:"totalMonthly"` TotalFees int `json:"totalFees"` TotalCredits int `json:"totalCredits"` CashToClose int `json:"cashToClose"` } type Estimate struct { Id int `json:"id"` User int `json:"user"` Borrower Borrower `json:"borrower"` Transaction string `json:"transaction"` Price int `json:"price"` Property string `json:"property"` Occupancy string `json:"occupancy"` Zip string `json:"zip"` Pud bool `json:"pud"` Loans []Loan `json:"loans"` Results []Result `json:"results"` } var ( regexen = make(map[string]*regexp.Regexp) relock sync.Mutex address = "127.0.0.1:8001" ) var paths = map[string]string { "home": "home.tpl", "terms": "terms.tpl", "app": "app.tpl", } var pages = map[string]Page { "home": cache("home", "Home"), "terms": cache("terms", "Terms and Conditions"), "app": cache("app", "App"), } var roles = map[string]int{ "User": 1, "Manager": 2, "Admin": 3, } // Used to validate claim in JWT token body. Checks if user id is greater than // zero and time format is valid func (c UserClaims) Valid() error { if c.Id < 1 { return errors.New("Invalid id") } t, err := time.Parse(time.UnixDate, c.Exp) if err != nil { return err } if t.Before(time.Now()) { return errors.New("Token expired.") } return err } func cache(name string, title string) Page { var p = []string{"master.tpl", paths[name]} tpl := template.Must(template.ParseFiles(p...)) return Page{tpl: tpl, Title: title, Name: name, } } func (page Page) Render(w http.ResponseWriter) { err := page.tpl.Execute(w, page) if err != nil { log.Print(err) } } func match(path, pattern string, args *[]string) bool { relock.Lock() defer relock.Unlock() regex := regexen[pattern] if regex == nil { regex = regexp.MustCompile("^" + pattern + "$") regexen[pattern] = regex } matches := regex.FindStringSubmatch(path) if len(matches) <= 0 { return false } *args = matches[1:] return true } func makeResults(estimate Estimate) ([]Result) { var results []Result amortize := func(principle float64, rate float64, periods float64) int { exp := math.Pow(1+rate, periods) return int(principle * rate * exp / (exp - 1)) } for _, loan := range estimate.Loans { var result Result // Monthly payments use amortized loan payment formula plus monthly MI, // plus all other recurring fees result.LoanPayment = amortize(float64(loan.Amount), float64(loan.Interest / 100 / 12), float64(loan.Term * 12)) result.TotalMonthly = result.LoanPayment + loan.Hoi + loan.Tax + loan.Hazard if loan.Mi.Monthly { result.TotalMonthly = result.TotalMonthly + int(loan.Mi.Rate/100*float32(loan.Amount)) } for i := range loan.Fees { if loan.Fees[i].Amount > 0 { result.TotalFees = result.TotalFees + loan.Fees[i].Amount } else { result.TotalCredits = result.TotalCredits + loan.Fees[i].Amount } } result.CashToClose = result.TotalFees + result.TotalCredits + (estimate.Price - loan.Amount) result.LoanId = loan.Id results = append(results, result) } return results } 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 := makeResults(estimate) json.NewEncoder(w).Encode(results) } func getLoanType( db *sql.DB, id int) (LoanType, error) { types, err := getLoanTypes(db, id, 0, 0) if err != nil { return LoanType{Id: id}, err } if len(types) == 0 { return LoanType{Id: id}, errors.New("No type with that id") } return types[0], nil } func getLoanTypes( db *sql.DB, id int, user int, branch int ) ( []LoanType, error) { var loans []LoanType var query = `SELECT id, user_id, branch_id, name FROM loan_type WHERE loan_type.id = CASE @e := ? WHEN 0 THEN id ELSE @e END OR loan_type.user_id = CASE @e := ? WHEN 0 THEN id ELSE @e END OR loan_type.branch_id = CASE @e := ? WHEN 0 THEN id ELSE @e END` // Should be changed to specify user rows, err := db.Query(query, id, user, branch) if err != nil { return nil, fmt.Errorf("loan_type error: %v", err) } defer rows.Close() for rows.Next() { var loan LoanType if err := rows.Scan( &loan.Id, &loan.User, &loan.Branch, &loan.Name) err != nil { log.Printf("Error occured fetching loan: %v", err) return nil, fmt.Errorf("Error occured fetching loan: %v", err) } loans = append(loans, loan) } return loans, nil } func getFees(db *sql.DB, loan int) ([]Fee, error) { var fees []Fee query := `SELECT id, loan_id, amount, perc, type, notes, name, category FROM fee WHERE loan_id = ?` rows, err := db.Query(query, loan) if err != nil { return nil, fmt.Errorf("Fee query error %v", err) } defer rows.Close() for rows.Next() { var fee Fee if err := rows.Scan( &fee.Id, &fee.LoanId, &fee.Amount, &fee.Perc, &fee.Type, &fee.Notes, &fee.Name, &fee.Category, ) err != nil { return nil, fmt.Errorf("Fees scanning error: %v", err) } fees = append(fees, fee) } return fees, nil } func fetchFeesTemp(db *sql.DB, user int, branch int) ([]FeeTemplate, error) { var fees []FeeTemplate rows, err := db.Query( "SELECT * FROM fee_template " + "WHERE user_id = ? OR branch_id = ?", user, branch) if err != nil { return nil, fmt.Errorf("Fee template query error %v", err) } defer rows.Close() for rows.Next() { var fee FeeTemplate if err := rows.Scan( &fee.Id, &fee.User, &fee.Branch, &fee.Amount, &fee.Perc, &fee.Type, &fee.Notes, &fee.Name, &fee.Category, &fee.Auto) err != nil { return nil, fmt.Errorf("FeesTemplate scanning error: %v", err) } fees = append(fees, fee) } return fees, nil } // Fetch fees from the database func getFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) { var fees []FeeTemplate claims, err := getClaims(r) if err != nil { w.WriteHeader(500); return } users, err := queryUsers(db, claims.Id) if err != nil { w.WriteHeader(422); return } fees, err = fetchFeesTemp(db, claims.Id, users[0].BranchId) json.NewEncoder(w).Encode(fees) } func getMi(db *sql.DB, loan int) (MI, error) { var mi MI query := `SELECT type, label, lender, rate, premium, upfront, five_year_total, initial_premium, initial_rate, initial_amount FROM mi WHERE loan_id = ?` rows, err := db.Query(query, loan) if err != nil { return mi, err } defer rows.Close() if (!rows.Next()) { return mi, nil } if err := rows.Scan( &mi.Type, &mi.Label, &mi.Lender, &mi.Rate, &mi.Premium, &mi.Upfront, &mi.FiveYearTotal, &mi.InitialAllInPremium, &mi.InitialAllInRate, &mi.InitialAmount, ) err != nil { return mi, err } return mi, nil } func getBorrower(db *sql.DB, id int) (Borrower, error) { var borrower Borrower row := db.QueryRow( "SELECT * FROM borrower " + "WHERE id = ? LIMIT 1", id) if err := row.Scan( &borrower.Id, &borrower.Credit, &borrower.Income, &borrower.Num, ) err != nil { return borrower, fmt.Errorf("Borrower scanning error: %v", err) } return borrower, nil } // Query Lender APIs and parse responses into MI structs func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI { var err error var loan Loan = estimate.Loans[pos] var ltv = func(l float32) string { switch { case l > 95: return "LTV97" case l > 90: return "LTV95" case l > 85: return "LTV90" default: return "LTV85" } } var term = func(t int) string { switch { case t <= 10: return "A10" case t <= 15: return "A15" case t <= 20: return "A20" case t <= 25: return "A25" case t <= 30: return "A30" default: return "A40" } } var propertyCodes = map[string]string { "Single Attached": "SFO", "Single Detached": "SFO", "Condo Lo-rise": "CON", "Condo Hi-rise": "CON", } var purposeCodes = map[string]string { "Purchase": "PUR", "Refinance": "RRT", } body, err := json.Marshal(map[string]any{ "zipCode": estimate.Zip, "stateCode": "CA", "address": "", "propertyTypeCode": propertyCodes[estimate.Property], "occupancyTypeCode": "PRS", "loanPurposeCode": purposeCodes[estimate.Transaction], "loanAmount": loan.Amount, "loanToValue": ltv(loan.Ltv), "amortizationTerm": term(loan.Term), "loanTypeCode": "FXD", "duLpDecisionCode": "DAE", "loanProgramCodes": []any{}, "debtToIncome": loan.Dti, "wholesaleLoan": 0, "coveragePercentageCode": "L30", "productCode": "BPM", "renewalTypeCode": "CON", "numberOfBorrowers": 1, "coBorrowerCreditScores": []any{}, "borrowerCreditScore": strconv.Itoa(estimate.Borrower.Credit), "masterPolicy": nil, "selfEmployedIndicator": false, "armType": "", "userId": 44504, }) if err != nil { log.Printf("Could not marshal NationalMI body: \n%v\n%v\n", bytes.NewBuffer(body), err) } req, err := http.NewRequest("POST", "https://rate-gps.nationalmi.com/rates/productRateQuote", bytes.NewBuffer(body)) req.Header.Add("Content-Type", "application/json") req.AddCookie(&http.Cookie{ Name: "nmirategps_email", Value: config["NationalMIEmail"]}) resp, err := http.DefaultClient.Do(req) var res map[string]interface{} var result []MI if resp.StatusCode != 200 { log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n", resp.Status, resp, req.Body, bytes.NewBuffer(body)) } else { json.NewDecoder(resp.Body).Decode(&res) // estimate.Loans[pos].Mi = res // Parse res into result here } return result } // Make comparison PDF func generatePDF(w http.ResponseWriter, db *sql.DB, r *http.Request) { } func login(w http.ResponseWriter, db *sql.DB, r *http.Request) { var id int var role string var err error var user User json.NewDecoder(r.Body).Decode(&user) row := db.QueryRow( `SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`, user.Email, user.Password, ) err = row.Scan(&id, &role) if err != nil { http.Error(w, "Invalid Credentials.", http.StatusUnauthorized) return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{ Id: id, Role: role, Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)}) tokenStr, err := token.SignedString([]byte(config["JWT_SECRET"])) if err != nil { log.Println("Token could not be signed: ", err, tokenStr) http.Error(w, "Token generation error.", http.StatusInternalServerError) return } cookie := http.Cookie{Name: "skouter", Value: tokenStr, Path: "/", Expires: time.Now().Add(time.Hour * 24)} http.SetCookie(w, &cookie) _, err = w.Write([]byte(tokenStr)) if err != nil { http.Error(w, "Could not complete token write.", http.StatusInternalServerError)} } func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) // Will verify existing signature and expiry time token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{ Id: claims.Id, Role: claims.Role, Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)}) tokenStr, err := token.SignedString([]byte(config["JWT_SECRET"])) if err != nil { log.Println("Token could not be signed: ", err, tokenStr) http.Error(w, "Token generation error.", http.StatusInternalServerError) return } cookie := http.Cookie{Name: "skouter", Value: tokenStr, Path: "/", Expires: time.Now().Add(time.Hour * 24)} http.SetCookie(w, &cookie) _, err = w.Write([]byte(tokenStr)) if err != nil { http.Error(w, "Could not complete token write.", http.StatusInternalServerError)} } func getClaims(r *http.Request) (UserClaims, error) { claims := new(UserClaims) _, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ") if !found { return *claims, errors.New("Token not found") } // Pull token payload into UserClaims _, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) { return []byte(config["JWT_SECRET"]), nil }) if err != nil { return *claims, err } if err = claims.Valid(); err != nil { return *claims, err } return *claims, nil } func guard(r *http.Request, required int) bool { claims, err := getClaims(r) if err != nil { return false } if roles[claims.Role] < required { return false } return true } func queryUsers(db *sql.DB, id int) ( []User, error ) { var users []User var query string var rows *sql.Rows var err error query = `SELECT u.id, u.email, u.first_name, u.last_name, u.branch_id, u.country, u.title, u.status, u.verified, u.role FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END ` rows, err = db.Query(query, id) if err != nil { return users, err } defer rows.Close() for rows.Next() { var user User if err := rows.Scan( &user.Id, &user.Email, &user.FirstName, &user.LastName, &user.BranchId, &user.Country, &user.Title, &user.Status, &user.Verified, &user.Role, ) err != nil { return users, err } users = append(users, user) } // Prevents runtime panics if len(users) == 0 { return users, errors.New("User not found.") } return users, nil } func insertResults(db *sql.DB, results []Result) (error){ var query string var row *sql.Row var err error query = `INSERT INTO estimate_result ( loan_id, loan_payment, total_monthly, total_fees, total_credits, cash_to_close ) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id ` for i := range results { db.QueryRow(query, results[i].LoanId, results[i].LoanPayment, results[i].TotalMonthly, results[i].TotalFees, results[i].TotalCredits, results[i].CashToClose, ) err = row.Scan(&results[i].Id) if err != nil { return err } } return nil } func insertUser(db *sql.DB, user User) (User, error){ var query string var row *sql.Row var err error var id int // Inserted user's id query = `INSERT INTO user ( email, first_name, last_name, password, created, role, verified, last_login ) VALUES (?, ?, ?, sha2(?, 256), NOW(), ?, ?, NOW()) RETURNING id ` row = db.QueryRow(query, user.Email, user.FirstName, user.LastName, user.Password, user.Role, user.Verified, ) err = row.Scan(&id) if err != nil { return User{}, err } users, err := queryUsers(db, id) if err != nil { return User{}, err } return users[0], nil } func updateUser(user User, db *sql.DB) error { query := ` UPDATE user SET email = CASE @e := ? WHEN '' THEN email ELSE @e END, first_name = CASE @fn := ? WHEN '' THEN first_name ELSE @fn END, last_name = CASE @ln := ? WHEN '' THEN last_name ELSE @ln END, role = CASE @r := ? WHEN '' THEN role ELSE @r END, password = CASE @p := ? WHEN '' THEN password ELSE sha2(@p, 256) END WHERE id = ? ` _, err := db.Exec(query, user.Email, user.FirstName, user.LastName, user.Role, user.Password, user.Id, ) return err } func getUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) if err != nil { w.WriteHeader(500); return } users, err := queryUsers(db, claims.Id) if err != nil { w.WriteHeader(422); log.Println(err); return } json.NewEncoder(w).Encode(users) } func getUsers(w http.ResponseWriter, db *sql.DB, r *http.Request) { users, err := queryUsers(db, 0) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } json.NewEncoder(w).Encode(users) } // Updates a user using only specified values in the JSON body func patchUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) _, err = mail.ParseAddress(user.Email) if err != nil { http.Error(w, "Invalid email.", 422); return } if roles[user.Role] == 0 { http.Error(w, "Invalid role.", 422) return } err = updateUser(user, db) if err != nil { http.Error(w, "Bad form values.", 422); return } users, err := queryUsers(db, user.Id) if err != nil { http.Error(w, "Bad form values.", 422); return } json.NewEncoder(w).Encode(users[0]) } // Update specified fields of the user specified in the claim func patchSelf(w http.ResponseWriter, db *sql.DB, r *http.Request) { claim, err := getClaims(r) var user User json.NewDecoder(r.Body).Decode(&user) // First check that the target user to be updated is the same as the claim id, and // their role is unchanged. if err != nil || claim.Id != user.Id { http.Error(w, "Target user's id does not match claim.", 401) return } if claim.Role != user.Role && user.Role != "" { http.Error(w, "Administrator required to escalate role.", 401) return } patchUser(w, db, r) } func deleteUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, "Invalid fields.", 422) return } query := `DELETE FROM user WHERE id = ?` _, err = db.Exec(query, user.Id) if err != nil { http.Error(w, "Could not delete.", 500) } } func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, "Invalid fields.", 422); return } _, err = mail.ParseAddress(user.Email) if err != nil { http.Error(w, "Invalid email.", 422); return } if roles[user.Role] == 0 { http.Error(w, "Invalid role.", 422) } user, err = insertUser(db, user) if err != nil { http.Error(w, "Error creating user.", 422); return } 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 getResults(db *sql.DB, e int, id int) ( []Result, error ) { var results []Result var query string var rows *sql.Rows var err error query = `SELECT r.id, loan_id, loan_payment, total_monthly, total_fees, total_credits, cash_to_close FROM estimate_result r INNER JOIN loan ON r.loan_id = loan.id WHERE r.id = CASE @e := ? WHEN 0 THEN r.id ELSE @e END AND loan.estimate_id = ? ` rows, err = db.Query(query, id, e) if err != nil { return results, err } defer rows.Close() for rows.Next() { var result Result if err := rows.Scan( &result.Id, &result.LoanId, &result.LoanPayment, &result.TotalMonthly, &result.TotalFees, &result.TotalCredits, &result.CashToClose, ) err != nil { return results, err } results = append(results, result) } // Prevents runtime panics // if len(results) == 0 { return results, errors.New("Result not found.") } return results, nil } // Must have an estimate ID 'e', but not necessarily a loan id 'id' func getLoans(db *sql.DB, e int, id int) ( []Loan, error ) { var loans []Loan var query string var rows *sql.Rows var err error query = `SELECT l.id, l.type_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.Type.Id, &loan.EstimateId, &loan.Amount, &loan.Term, &loan.Interest, &loan.Ltv, &loan.Dti, &loan.Hoi, &loan.Tax, &loan.Name, ) err != nil { return loans, err } mi, err := getMi(db, loan.Id) if err != nil { return loans, err } loan.Mi = mi fees, err := getFees(db, loan.Id) if err != nil { return loans, err } loan.Fees = fees loan.Type, err = getLoanType(db, loan.Type.Id) if 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 getEstimates(db *sql.DB, id int, user int) ( []Estimate, error ) { var estimates []Estimate var query string var rows *sql.Rows var err error query = `SELECT id, user_id, borrower_id, transaction, price, property, occupancy, zip, pud FROM estimate WHERE id = CASE @e := ? WHEN 0 THEN id ELSE @e END AND user_id = CASE @e := ? WHEN 0 THEN user_id ELSE @e END ` rows, err = db.Query(query, id, user) 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 } borrower, err := getBorrower(db, estimate.Borrower.Id) if err != nil { return estimates, err } estimate.Borrower = borrower estimate.Results, err = getResults(db, estimate.Id, 0) if err != nil { return estimates, err } estimates = append(estimates, estimate) } // Prevents runtime panics if len(estimates) == 0 { return estimates, errors.New("Estimate not found.") } for i := range estimates { estimates[i].Loans, err = getLoans(db, estimates[i].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 insertMi(db *sql.DB, mi MI) (int, error) { var id int query := `INSERT INTO mi ( type, label, lender, rate, premium, upfront, five_year_total, initial_premium, initial_rate, initial_amount ) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id` row := db.QueryRow(query, &mi.Type, &mi.Label, &mi.Lender, &mi.Rate, &mi.Premium, &mi.Upfront, &mi.FiveYearTotal, &mi.InitialAllInPremium, &mi.InitialAllInRate, &mi.InitialAmount, ) err := row.Scan(&id) if err != nil { return 0, err } return id, nil } func insertFee(db *sql.DB, fee Fee) (int, error) { var id int query := `INSERT INTO fee (loan_id, amount, perc, type, notes, name, category) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id` row := db.QueryRow(query, fee.LoanId, fee.Amount, fee.Perc, fee.Type, fee.Notes, fee.Name, fee.Category, ) 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 } _, err = insertMi(db, loan.Mi) if err != nil { return loan, err } for i := range loan.Fees { _, err := insertFee(db, loan.Fees[i]) if err != nil { return loan, err } } loans, err := getLoans(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 := getEstimates(db, estimate.Id, 0) 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 } claims, err := getClaims(r) estimate.User = claims.Id estimate, err = insertEstimate(db, estimate) if err != nil { http.Error(w, err.Error(), 422); return } estimate.Results = makeResults(estimate) err = insertResults(db, estimate.Results) if err != nil { http.Error(w, err.Error(), 500); return } e, err := getEstimates(db, estimate.Id, 0) if err != nil { http.Error(w, err.Error(), 500); return } json.NewEncoder(w).Encode(e[0]) } // Query all estimates that belong to the current user func fetchEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimates []Estimate claims, err := getClaims(r) estimates, err = getEstimates(db, 0, claims.Id) if err != nil { http.Error(w, err.Error(), 500); return } json.NewEncoder(w).Encode(estimates) } 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 } // Loan option for veterans with no set rules func checkVA(l Loan, b Borrower) error { return nil } // Loan option for residents of rural areas with no set rules func checkUSDA(l Loan, b Borrower) error { 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.Id { case 1: err = checkConventional(l, e.Borrower) case 2: err = checkFHA(l, e.Borrower) case 3: err = checkVA(l, e.Borrower) case 4: err = checkUSDA(l, e.Borrower) default: err = errors.New("Invalid loan type") } if err != nil { return err } } 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, err.Error(), 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 p := r.URL.Path db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter", config["DBUser"], config["DBPass"])) w.Header().Set("Content-Type", "application/json; charset=UTF-8") err = db.Ping() if err != nil { fmt.Println("Bad database configuration: %v\n", err) panic(err) // maybe os.Exit(1) instead } switch { case match(p, "/api/login", &args) && r.Method == http.MethodPost: login(w, db, r) case match(p, "/api/token", &args) && r.Method == http.MethodGet && guard(r, 1): getToken(w, db, r) case match(p, "/api/users", &args) && // Array of all users r.Method == http.MethodGet && guard(r, 3): getUsers(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodGet && guard(r, 1): getUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPost && guard(r, 3): createUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPatch && guard(r, 3): // For admin to modify any user patchUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPatch && guard(r, 2): // For employees to modify own accounts patchSelf(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodDelete && guard(r, 3): deleteUser(w, db, r) case match(p, "/api/fees", &args) && r.Method == http.MethodGet && guard(r, 1): getFeesTemp(w, db, r) case match(p, "/api/estimates", &args) && r.Method == http.MethodGet && guard(r, 1): fetchEstimate(w, db, r) case match(p, "/api/estimate", &args) && 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) case match(p, "/api/estimate/summarize", &args) && r.Method == http.MethodPost && guard(r, 1): summarize(w, db, r) default: http.Error(w, "Invalid route or token", 404) } db.Close() } func route(w http.ResponseWriter, r *http.Request) { var page Page var args []string p := r.URL.Path switch { case r.Method == "GET" && match(p, "/", &args): page = pages[ "home" ] case match(p, "/terms", &args): page = pages[ "terms" ] case match(p, "/app", &args): page = pages[ "app" ] default: http.NotFound(w, r) return } page.Render(w) } func main() { files := http.FileServer(http.Dir("")) http.Handle("/assets/", files) http.HandleFunc("/api/", api) http.HandleFunc("/", route) log.Fatal(http.ListenAndServe(address, nil)) }