Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

skouter.go 12 KiB

2 lat temu
2 lat temu
2 lat temu
2 lat temu
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  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. }