Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
 
 
 
 
 
 

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