Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

591 lines
12 KiB

  1. package main
  2. import (
  3. "net/http"
  4. "log"
  5. "sync"
  6. "regexp"
  7. "html/template"
  8. "database/sql"
  9. _ "github.com/go-sql-driver/mysql"
  10. "fmt"
  11. "encoding/json"
  12. // "io"
  13. "strconv"
  14. "bytes"
  15. )
  16. type Page struct {
  17. tpl *template.Template
  18. Title string
  19. Name string
  20. }
  21. type Borrower struct {
  22. Id int `json:"id"`
  23. Credit int `json:"credit"`
  24. Income int `json:"income"`
  25. Num int `json:"num"`
  26. }
  27. type FeeTemplate struct {
  28. Id int `json:"id"`
  29. User int `json:"user"`
  30. Branch int `json:"branch"`
  31. Amount int `json:"amount"`
  32. Perc int `json:"perc"`
  33. Type string `json:"type"`
  34. Notes string `json:"notes"`
  35. Name string `json:"name"`
  36. Category string `json:"category"`
  37. Auto bool `json:"auto"`
  38. }
  39. type Fee struct {
  40. Id int `json:"id"`
  41. LoanId int `json:"loan_id"`
  42. Amount int `json:"amount"`
  43. Perc int `json:"perc"`
  44. Type string `json:"type"`
  45. Notes string `json:"notes"`
  46. Name string `json:"name"`
  47. Category string `json:"category"`
  48. }
  49. type LoanType struct {
  50. Id int `json:"id"`
  51. User int `json:"user"`
  52. Branch int `json:"branch"`
  53. Name string `json:"name"`
  54. }
  55. type Loan struct {
  56. Id int `json:id`
  57. EstimateId int `json:estimate_id`
  58. Type LoanType `json:"loanType"`
  59. Amount int `json:"loanAmount"`
  60. Term int `json:"term"`
  61. Ltv float32 `json:"ltv"`
  62. Dti float32 `json:"dti"`
  63. Hoi int `json:"hoi"`
  64. Interest int `json:"interest"`
  65. Mi MI `json:"mi"`
  66. Fees []Fee `json:"fees"`
  67. Name string `json:"name"`
  68. }
  69. type MI struct {
  70. Type string
  71. Label string
  72. Lender string
  73. Rate float32
  74. Premium float32
  75. Upfront float32
  76. FiveYearTotal float32
  77. InitialAllInPremium float32
  78. InitialAllInRate float32
  79. InitialAmount float32
  80. }
  81. type Estimate struct {
  82. Id int `json:"id"`
  83. User int `json:"user"`
  84. Borrower Borrower `json:"borrower"`
  85. Transaction string `json:"transaction"`
  86. Price int `json:"price"`
  87. Property string `json:"property"`
  88. Occupancy string `json:"occupancy"`
  89. Zip string `json:"zip"`
  90. Pud bool `json:"pud"`
  91. Loans []Loan `json:"loans"`
  92. }
  93. var (
  94. regexen = make(map[string]*regexp.Regexp)
  95. relock sync.Mutex
  96. address = "127.0.0.1:8001"
  97. )
  98. var paths = map[string]string {
  99. "home": "home.tpl",
  100. "terms": "terms.tpl",
  101. "app": "app.tpl",
  102. }
  103. var pages = map[string]Page {
  104. "home": cache("home", "Home"),
  105. "terms": cache("terms", "Terms and Conditions"),
  106. "app": cache("app", "App"),
  107. }
  108. func cache(name string, title string) Page {
  109. var p = []string{"master.tpl", paths[name]}
  110. tpl := template.Must(template.ParseFiles(p...))
  111. return Page{tpl: tpl,
  112. Title: title,
  113. Name: name,
  114. }
  115. }
  116. func (page Page) Render(w http.ResponseWriter) {
  117. err := page.tpl.Execute(w, page)
  118. if err != nil {
  119. log.Print(err)
  120. }
  121. }
  122. func match(path, pattern string, args *[]string) bool {
  123. relock.Lock()
  124. defer relock.Unlock()
  125. regex := regexen[pattern]
  126. if regex == nil {
  127. regex = regexp.MustCompile("^" + pattern + "$")
  128. regexen[pattern] = regex
  129. }
  130. matches := regex.FindStringSubmatch(path)
  131. if len(matches) <= 0 {
  132. return false
  133. }
  134. *args = matches[1:]
  135. return true
  136. }
  137. func getLoanType(
  138. db *sql.DB,
  139. user int,
  140. branch int,
  141. isUser bool) ([]LoanType, error) {
  142. var loans []LoanType
  143. // Should be changed to specify user
  144. rows, err :=
  145. db.Query(`SELECT * FROM loan_type WHERE user_id = ? AND branch_id = ? ` +
  146. "OR (user_id = 0 AND branch_id = 0)", user, branch)
  147. if err != nil {
  148. return nil, fmt.Errorf("loan_type error: %v", err)
  149. }
  150. defer rows.Close()
  151. for rows.Next() {
  152. var loan LoanType
  153. if err := rows.Scan(
  154. &loan.Id,
  155. &loan.User,
  156. &loan.Branch,
  157. &loan.Name)
  158. err != nil {
  159. log.Printf("Error occured fetching loan: %v", err)
  160. return nil, fmt.Errorf("Error occured fetching loan: %v", err)
  161. }
  162. loans = append(loans, loan)
  163. }
  164. log.Printf("The loans: %v", loans)
  165. return loans, nil
  166. }
  167. func getEstimate(db *sql.DB, id int) (Estimate, error) {
  168. var estimate Estimate
  169. var err error
  170. query := `SELECT e.id, e.user_id, e.transaction,
  171. e.price, e.property, e.occupancy, e.zip, e.pud,
  172. b.id, b.credit_score, b.monthly_income, b.num
  173. FROM estimate e
  174. INNER JOIN borrower b ON e.borrower_id = b.id
  175. WHERE e.id = ?
  176. `
  177. // Inner join should always be valid because a borrower is a required
  178. // foreign key.
  179. row := db.QueryRow(query, id)
  180. if err = row.Scan(
  181. &estimate.Id,
  182. &estimate.User,
  183. &estimate.Transaction,
  184. &estimate.Price,
  185. &estimate.Property,
  186. &estimate.Occupancy,
  187. &estimate.Zip,
  188. &estimate.Pud,
  189. &estimate.Borrower.Id,
  190. &estimate.Borrower.Credit,
  191. &estimate.Borrower.Income,
  192. &estimate.Borrower.Num,
  193. )
  194. err != nil {
  195. return estimate, fmt.Errorf("Estimate scanning error: %v", err)
  196. }
  197. estimate.Loans, err = getLoans(db, estimate.Id)
  198. return estimate, err
  199. }
  200. func getFees(db *sql.DB, loan int) ([]Fee, error) {
  201. var fees []Fee
  202. rows, err := db.Query(
  203. "SELECT * FROM fees " +
  204. "WHERE loan_id = ?",
  205. loan)
  206. if err != nil {
  207. return nil, fmt.Errorf("Fee query error %v", err)
  208. }
  209. defer rows.Close()
  210. for rows.Next() {
  211. var fee Fee
  212. if err := rows.Scan(
  213. &fee.Id,
  214. &fee.LoanId,
  215. &fee.Amount,
  216. &fee.Perc,
  217. &fee.Type,
  218. &fee.Notes,
  219. &fee.Name,
  220. &fee.Category,
  221. )
  222. err != nil {
  223. return nil, fmt.Errorf("Fees scanning error: %v", err)
  224. }
  225. fees = append(fees, fee)
  226. }
  227. return fees, nil
  228. }
  229. // Fetch fees from the database
  230. func getFeesTemp(db *sql.DB, user int) ([]FeeTemplate, error) {
  231. var fees []FeeTemplate
  232. rows, err := db.Query(
  233. "SELECT * FROM fee_template " +
  234. "WHERE user_id = ? OR user_id = 0",
  235. user)
  236. if err != nil {
  237. return nil, fmt.Errorf("Fee template query error %v", err)
  238. }
  239. defer rows.Close()
  240. for rows.Next() {
  241. var fee FeeTemplate
  242. if err := rows.Scan(
  243. &fee.Id,
  244. &fee.User,
  245. &fee.Branch,
  246. &fee.Amount,
  247. &fee.Perc,
  248. &fee.Type,
  249. &fee.Notes,
  250. &fee.Name,
  251. &fee.Category,
  252. &fee.Auto)
  253. err != nil {
  254. return nil, fmt.Errorf("FeesTemplate scanning error: %v", err)
  255. }
  256. fees = append(fees, fee)
  257. }
  258. return fees, nil
  259. }
  260. func getMi(db *sql.DB, loan int) (MI, error) {
  261. var mi MI
  262. query := `SELECT
  263. type, label, lender, rate, premium, upfront, five_year_total,
  264. initial_premium, initial_rate, initial_amount
  265. FROM mi WHERE loan_id = ?`
  266. row := db.QueryRow(query, loan)
  267. if err := row.Scan(
  268. &mi.Type,
  269. &mi.Label,
  270. &mi.Lender,
  271. &mi.Rate,
  272. &mi.Premium,
  273. &mi.Upfront,
  274. &mi.FiveYearTotal,
  275. &mi.InitialAllInPremium,
  276. &mi.InitialAllInRate,
  277. &mi.InitialAmount,
  278. )
  279. err != nil {
  280. return mi, err
  281. }
  282. return mi, nil
  283. }
  284. func getLoans(db *sql.DB, estimate int) ([]Loan, error) {
  285. var loans []Loan
  286. query := `SELECT
  287. l.id, l.amount, l.term, l.interest, l.ltv, l.dti, l.hoi,
  288. lt.id, lt.user_id, lt.branch_id, lt.name
  289. FROM loan l INNER JOIN loan_type lt ON l.type_id = lt.id
  290. WHERE l.estimate_id = ?
  291. `
  292. rows, err := db.Query(query, estimate)
  293. if err != nil {
  294. return nil, fmt.Errorf("Loan query error %v", err)
  295. }
  296. defer rows.Close()
  297. for rows.Next() {
  298. var loan Loan
  299. if err := rows.Scan(
  300. &loan.Id,
  301. &loan.Amount,
  302. &loan.Term,
  303. &loan.Interest,
  304. &loan.Ltv,
  305. &loan.Dti,
  306. &loan.Hoi,
  307. &loan.Type.Id,
  308. &loan.Type.User,
  309. &loan.Type.Branch,
  310. &loan.Type.Name,
  311. )
  312. err != nil {
  313. return loans, fmt.Errorf("Loans scanning error: %v", err)
  314. }
  315. mi, err := getMi(db, loan.Id)
  316. if err != nil {
  317. return loans, err
  318. }
  319. loan.Mi = mi
  320. loans = append(loans, loan)
  321. }
  322. return loans, nil
  323. }
  324. func getBorrower(db *sql.DB, id int) (Borrower, error) {
  325. var borrower Borrower
  326. row := db.QueryRow(
  327. "SELECT * FROM borrower " +
  328. "WHERE id = ? LIMIT 1",
  329. id)
  330. if err := row.Scan(
  331. &borrower.Id,
  332. &borrower.Credit,
  333. &borrower.Income,
  334. &borrower.Num,
  335. )
  336. err != nil {
  337. return borrower, fmt.Errorf("Borrower scanning error: %v", err)
  338. }
  339. return borrower, nil
  340. }
  341. // Query Lender APIs and parse responses into MI structs
  342. func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {
  343. var err error
  344. var loan Loan = estimate.Loans[pos]
  345. var ltv = func(l float32) string {
  346. switch {
  347. case l > 95: return "LTV97"
  348. case l > 90: return "LTV95"
  349. case l > 85: return "LTV90"
  350. default: return "LTV85"
  351. }
  352. }
  353. var term = func(t int) string {
  354. switch {
  355. case t <= 10: return "A10"
  356. case t <= 15: return "A15"
  357. case t <= 20: return "A20"
  358. case t <= 25: return "A25"
  359. case t <= 30: return "A30"
  360. default: return "A40"
  361. }
  362. }
  363. var propertyCodes = map[string]string {
  364. "Single Attached": "SFO",
  365. "Single Detached": "SFO",
  366. "Condo Lo-rise": "CON",
  367. "Condo Hi-rise": "CON",
  368. }
  369. var purposeCodes = map[string]string {
  370. "Purchase": "PUR",
  371. "Refinance": "RRT",
  372. }
  373. body, err := json.Marshal(map[string]any{
  374. "zipCode": estimate.Zip,
  375. "stateCode": "CA",
  376. "address": "",
  377. "propertyTypeCode": propertyCodes[estimate.Property],
  378. "occupancyTypeCode": "PRS",
  379. "loanPurposeCode": purposeCodes[estimate.Transaction],
  380. "loanAmount": loan.Amount,
  381. "loanToValue": ltv(loan.Ltv),
  382. "amortizationTerm": term(loan.Term),
  383. "loanTypeCode": "FXD",
  384. "duLpDecisionCode": "DAE",
  385. "loanProgramCodes": []any{},
  386. "debtToIncome": loan.Dti,
  387. "wholesaleLoan": 0,
  388. "coveragePercentageCode": "L30",
  389. "productCode": "BPM",
  390. "renewalTypeCode": "CON",
  391. "numberOfBorrowers": 1,
  392. "coBorrowerCreditScores": []any{},
  393. "borrowerCreditScore": strconv.Itoa(estimate.Borrower.Credit),
  394. "masterPolicy": nil,
  395. "selfEmployedIndicator": false,
  396. "armType": "",
  397. "userId": 44504,
  398. })
  399. if err != nil {
  400. log.Printf("Could not marshal NationalMI body: \n%v\n%v\n",
  401. bytes.NewBuffer(body), err)
  402. }
  403. req, err := http.NewRequest("POST",
  404. "https://rate-gps.nationalmi.com/rates/productRateQuote",
  405. bytes.NewBuffer(body))
  406. req.Header.Add("Content-Type", "application/json")
  407. req.AddCookie(&http.Cookie{
  408. Name: "nmirategps_email",
  409. Value: config["NationalMIEmail"]})
  410. resp, err := http.DefaultClient.Do(req)
  411. var res map[string]interface{}
  412. if resp.StatusCode != 200 {
  413. log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n",
  414. resp.Status, resp, req.Body, bytes.NewBuffer(body))
  415. } else {
  416. json.NewDecoder(resp.Body).Decode(&res)
  417. // estimate.Loans[pos].Mi = res
  418. }
  419. return estimate
  420. }
  421. func validateEstimate() {
  422. return
  423. }
  424. func route(w http.ResponseWriter, r *http.Request) {
  425. var page Page
  426. var args []string
  427. p := r.URL.Path
  428. switch {
  429. case r.Method == "GET" && match(p, "/", &args):
  430. page = pages[ "home" ]
  431. case match(p, "/terms", &args):
  432. page = pages[ "terms" ]
  433. case match(p, "/app", &args):
  434. page = pages[ "app" ]
  435. case match(p, "/assets", &args):
  436. page = pages[ "app" ]
  437. default:
  438. http.NotFound(w, r)
  439. return
  440. }
  441. page.Render(w)
  442. }
  443. func api(w http.ResponseWriter, r *http.Request) {
  444. var args []string
  445. // var response string
  446. p := r.URL.Path
  447. db, err := sql.Open("mysql",
  448. fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter",
  449. config["DBUser"],
  450. config["DBPass"]))
  451. w.Header().Set("Content-Type", "application/json; charset=UTF-8")
  452. err = db.Ping()
  453. if err != nil {
  454. print("Bad database configuration: %v", err)
  455. panic(err)
  456. // maybe os.Exit(1) instead
  457. }
  458. switch {
  459. case match(p, "/api/loans", &args):
  460. resp, err := getLoanType(db, 0, 0, true)
  461. if resp != nil {
  462. json.NewEncoder(w).Encode(resp)
  463. } else {
  464. json.NewEncoder(w).Encode(err)
  465. }
  466. case match(p, "/api/fees", &args):
  467. resp, err := getFeesTemp(db, 0)
  468. if resp != nil {
  469. json.NewEncoder(w).Encode(resp)
  470. } else {
  471. json.NewEncoder(w).Encode(err)
  472. }
  473. case match(p, "/api/mi", &args):
  474. var err error
  475. est, err := getEstimate(db, 1)
  476. if err != nil {
  477. json.NewEncoder(w).Encode(err)
  478. log.Println("error occured:", err)
  479. break
  480. }
  481. json.NewEncoder(w).Encode(fetchMi(db, &est, 0).Loans[0].Mi)
  482. // if err != nil {
  483. // json.NewEncoder(w).Encode(err)
  484. // break
  485. // } else {
  486. // json.NewEncoder(w).Encode(resp)
  487. // }
  488. }
  489. }
  490. func main() {
  491. files := http.FileServer(http.Dir(""))
  492. http.Handle("/assets/", files)
  493. http.HandleFunc("/api/", api)
  494. http.HandleFunc("/", route)
  495. log.Fatal(http.ListenAndServe(address, nil))
  496. }