| } | } | ||||
| func (m *apiV1Response) sendJson(w http.ResponseWriter) (ret []byte, e error) { | func (m *apiV1Response) sendJson(w http.ResponseWriter) (ret []byte, e error) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| tempMap := m.tmp | tempMap := m.tmp | ||||
| m.tmp = nil | m.tmp = nil | ||||
| if config.Debug { | if config.Debug { | ||||
| } | } | ||||
| func apiV1SendJson(result interface{}, w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1SendJson(result interface{}, w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| out, e := json.Marshal(result) | out, e := json.Marshal(result) | ||||
| if e != nil { | if e != nil { | ||||
| log.Warn("Cannot convert result to json ", result) | log.Warn("Cannot convert result to json ", result) |
| package main | |||||
| import ( | |||||
| "biukop.com/sfm/loan" | |||||
| "crypto/sha256" | |||||
| "errors" | |||||
| "fmt" | |||||
| log "github.com/sirupsen/logrus" | |||||
| "io" | |||||
| "io/ioutil" | |||||
| "net/http" | |||||
| "os" | |||||
| "path/filepath" | |||||
| "strconv" | |||||
| "time" | |||||
| ) | |||||
| func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| id := r.URL.Path[len(apiV1Prefix+"lender-upload/"):] //remove prefix | |||||
| intId, e := strconv.Atoi(id) | |||||
| if e != nil { | |||||
| log.Println("invalid id for upload get", id, e) | |||||
| apiV1Client403Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| ul := loan.Uploads{} | |||||
| e = ul.Read(int64(intId)) | |||||
| if e != nil { | |||||
| log.Println("no file uploaded", intId, e) | |||||
| apiV1Client404Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| //check local file first | |||||
| path := config.Uploads + strconv.FormatInt(ul.Id, 10) + ".uploads" | |||||
| if fileExists(path) { | |||||
| http.ServeFile(w, r, path) | |||||
| return | |||||
| } | |||||
| log.Error("Upload file not found on disk", ul) | |||||
| apiV1Server500Error(w, r) // bad request | |||||
| } | |||||
| func apiV1UploadsPost(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| id := r.URL.Path[len(apiV1Prefix+"lender-upload/"):] //remove prefix | |||||
| filename, e := saveUploadToFile(r) | |||||
| if e != nil { | |||||
| log.Println("no file uploaded", filename, e) | |||||
| apiV1Client404Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| intId, e := strconv.Atoi(id) | |||||
| if id != "" { | |||||
| if e != nil { | |||||
| log.Println("Error Getting File", e) | |||||
| apiV1Client404Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| updateUploads(int64(intId), filename, w, r, ss) | |||||
| } else { | |||||
| createUploads(filename, w, r, ss) | |||||
| } | |||||
| } | |||||
| func sha256File(input io.Reader) string { | |||||
| hash := sha256.New() | |||||
| if _, err := io.Copy(hash, input); err != nil { | |||||
| log.Fatal(err) | |||||
| } | |||||
| sum := hash.Sum(nil) | |||||
| return fmt.Sprintf("%x", sum) | |||||
| } | |||||
| func updateUploads(id int64, fileName string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| ul := loan.Uploads{} | |||||
| e := ul.Read(int64(id)) | |||||
| if e != nil { | |||||
| log.Println("bad upload id is given ", id, e) | |||||
| apiV1Client404Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| ul1, _, e := saveUploadsToDB(id, fileName, r, ss) | |||||
| if e != nil { | |||||
| os.Remove(config.Uploads + ul.FileName) | |||||
| ul1.Delete() | |||||
| log.Println("cannot save file info to db ", e) | |||||
| apiV1Server500Error(w, r) // bad request | |||||
| return | |||||
| } | |||||
| apiV1SendJson(ul1, w, r, ss) | |||||
| } | |||||
| func createUploads(fileName string, w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| ul, _, e := saveUploadsToDB(0, fileName, r, ss) | |||||
| if e != nil { | |||||
| log.Println("cannot save file info to db ", e) | |||||
| e = ul.Delete() // delete the newly created, if failed, db will clean it | |||||
| if e != nil { | |||||
| log.Error("failed to remove unused uploads", ul) | |||||
| } | |||||
| e = os.Remove(config.Uploads + fileName) | |||||
| if e != nil { | |||||
| log.Error("failed to remove unused temp file", fileName) | |||||
| } | |||||
| apiV1Server500Error(w, r) // bad request | |||||
| return | |||||
| } | |||||
| apiV1SendJson(ul, w, r, ss) | |||||
| } | |||||
| func saveUploadsToDB(id int64, fileName string, | |||||
| r *http.Request, ss *loan.Session) (ul loan.Uploads, duplicate bool, e error) { | |||||
| duplicate = false | |||||
| e = r.ParseMultipartForm(10 << 20) // we should have ready parsed this, just in case | |||||
| if e != nil { | |||||
| return | |||||
| } | |||||
| file, header, e := r.FormFile("files") | |||||
| file.Seek(0, 0) //seek to beginning | |||||
| checksum := sha256File(file) | |||||
| ul.Id = id | |||||
| ul.Ts = time.Now() | |||||
| ul.FileName = header.Filename | |||||
| file.Seek(0, 0) //seek to beginning | |||||
| ul.Format = header.Header.Get("Content-type") | |||||
| ul.Size = header.Size // necessary to prevent duplicate | |||||
| ul.LastModified = 0 | |||||
| ul.Sha256 = checksum // necessary to prevent duplicate | |||||
| ul.By = ss.User | |||||
| e = ul.Write() // this Id will have the real Id if there is duplicates | |||||
| if e != nil { | |||||
| log.Error("Fail to update db ", ul, e) | |||||
| } else { | |||||
| if id > 0 && ul.Id != id { | |||||
| duplicate = true | |||||
| } | |||||
| target := fmt.Sprintf("%d.uploads", ul.Id) | |||||
| e = os.Rename(config.Uploads+fileName, config.Uploads+target) | |||||
| if e != nil { | |||||
| ul.FileName = fileName // some how failed to rename | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| func saveUploadToFile(r *http.Request) (filename string, e error) { | |||||
| e = r.ParseMultipartForm(10 << 20) | |||||
| if e != nil { | |||||
| return | |||||
| } | |||||
| file, header, e := r.FormFile("files") | |||||
| if e != nil { | |||||
| log.Println("Error Getting File", e) | |||||
| return | |||||
| } | |||||
| out, pathError := ioutil.TempFile(config.Uploads, "can-del-upload-*.tmp") | |||||
| if pathError != nil { | |||||
| log.Println("Error Creating a file for writing", pathError) | |||||
| return | |||||
| } | |||||
| out.Seek(0, 0) //seek to beginning | |||||
| size, e := io.Copy(out, file) | |||||
| if e != nil { | |||||
| os.Remove(out.Name()) //remove on failure | |||||
| log.Println("Error copying", e) | |||||
| return | |||||
| } | |||||
| if size != header.Size { | |||||
| e = errors.New("written file with incorrect size") | |||||
| } | |||||
| return filepath.Base(out.Name()), e | |||||
| } |
| {"DELETE", "payIn/", apiV1PayInDelete}, | {"DELETE", "payIn/", apiV1PayInDelete}, | ||||
| {"GET", "user-reward/", apiV1UserReward}, | {"GET", "user-reward/", apiV1UserReward}, | ||||
| {"GET", "login-available/", apiV1LoginAvailable}, | {"GET", "login-available/", apiV1LoginAvailable}, | ||||
| {"POST", "lender-upload/", apiV1UploadsPost}, | |||||
| {"GET", "lender-upload/", apiV1UploadsGet}, | |||||
| {"GET", "login", apiV1DumpRequest}, | {"GET", "login", apiV1DumpRequest}, | ||||
| } | } | ||||
| } else { //production | } else { //production | ||||
| {"DELETE", "payIn/", apiV1PayInDelete}, | {"DELETE", "payIn/", apiV1PayInDelete}, | ||||
| {"GET", "user-reward/", apiV1UserReward}, | {"GET", "user-reward/", apiV1UserReward}, | ||||
| {"GET", "login-available/", apiV1LoginAvailable}, | {"GET", "login-available/", apiV1LoginAvailable}, | ||||
| {"POST", "lender-upload/", apiV1UploadsPost}, | |||||
| {"GET", "lender-upload/", apiV1UploadsGet}, | |||||
| {"GET", "login", apiV1EmptyResponse}, | {"GET", "login", apiV1EmptyResponse}, | ||||
| } | } | ||||
| } | } | ||||
| //apiV1Main version 1 main entry for all REST API | //apiV1Main version 1 main entry for all REST API | ||||
| // | // | ||||
| func apiV1Main(w http.ResponseWriter, r *http.Request) { | func apiV1Main(w http.ResponseWriter, r *http.Request) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| //CORS setup | //CORS setup | ||||
| setupCrossOriginResponse(&w, r) | setupCrossOriginResponse(&w, r) | ||||
| } | } | ||||
| func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { | func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| w.WriteHeader(500) | w.WriteHeader(500) | ||||
| apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies | apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies | ||||
| } | } | ||||
| func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| w.WriteHeader(403) | w.WriteHeader(403) | ||||
| type struct403 struct { | type struct403 struct { | ||||
| Error int | Error int | ||||
| } | } | ||||
| func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| w.WriteHeader(404) | w.WriteHeader(404) | ||||
| type struct404 struct { | type struct404 struct { | ||||
| Error int | Error int | ||||
| } | } | ||||
| func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| //general setup | |||||
| w.Header().Set("Content-Type", "application/json;charset=UTF-8") | |||||
| apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies | apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies | ||||
| fmt.Fprintf(w, "") | fmt.Fprintf(w, "") | ||||
| } | } |
| func logRequestDebug(data []byte, err error) (ret string) { | func logRequestDebug(data []byte, err error) (ret string) { | ||||
| if err == nil { | if err == nil { | ||||
| ret = fmt.Sprintf("%s\n\n", string(data)) | |||||
| output := data | |||||
| if len(data) > 4096 { | |||||
| output = data[0:4095] | |||||
| } | |||||
| ret = fmt.Sprintf("%s\n\n", string(output)) | |||||
| fmt.Println(ret) | fmt.Println(ret) | ||||
| } else { | } else { | ||||
| log.Fatalf("%s\n\n", err) | log.Fatalf("%s\n\n", err) |
| package main | |||||
| import ( | |||||
| "biukop.com/sfm/loan" | |||||
| "errors" | |||||
| log "github.com/sirupsen/logrus" | |||||
| "net/http" | |||||
| "os" | |||||
| "os/exec" | |||||
| "strings" | |||||
| ) | |||||
| type FunderType string | |||||
| const ( | |||||
| Funder_AAA FunderType = "AAA Financial" | |||||
| Funder_Pepper = "Pepper" | |||||
| Funder_Resimac = "Resimac" | |||||
| Funder_Unknown = "cannot detect funder type" | |||||
| ) | |||||
| type AiDecodeIncome struct { | |||||
| Input struct { | |||||
| Uploads loan.Uploads | |||||
| FileName string //a local file on disk | |||||
| InMime string //may not be correct, just some suggestion only. | |||||
| } | |||||
| Mime string //mime actually detected. | |||||
| PayIn []loan.PayIn | |||||
| Funder FunderType | |||||
| AAA PayInAAAData | |||||
| } | |||||
| func decodePayInMain(filename string, format string) (ai AiDecodeIncome, e error) { | |||||
| ai.Input.FileName = filename | |||||
| ai.Input.InMime = format | |||||
| ai.PayIn = make([]loan.PayIn, 0, 10) | |||||
| ai.Mime, e = GetFileContentType(filename) | |||||
| if e != nil { | |||||
| return | |||||
| } | |||||
| switch ai.Mime { | |||||
| case "application/pdf": | |||||
| ai.decodePayInPdf(filename, format) | |||||
| } | |||||
| return ai, e | |||||
| } | |||||
| // tested, not accurate with xls, xlsx, it becomes zip and octstream sometime. | |||||
| func GetFileContentType(filename string) (contentType string, e error) { | |||||
| contentType = "" | |||||
| input, e := os.OpenFile(filename, os.O_RDONLY, 0755) | |||||
| // Only the first 512 bytes are used to sniff the content type. | |||||
| buffer := make([]byte, 512) | |||||
| _, e = input.Read(buffer) | |||||
| if e != nil { | |||||
| return | |||||
| } | |||||
| // Use the net/http package's handy DectectContentType function. Always returns a valid | |||||
| // content-type by returning "application/octet-stream" if no others seemed to match. | |||||
| contentType = http.DetectContentType(buffer) | |||||
| return | |||||
| } | |||||
| func (m *AiDecodeIncome) decodePayInPdf(filename string, format string) (ret []loan.PayIn, e error) { | |||||
| cmd := exec.Command("pdftotext", "-layout", filename, "-") | |||||
| //log.Println(cmd.String()) | |||||
| out, e := cmd.Output() | |||||
| if e != nil { | |||||
| log.Fatal(e) | |||||
| } | |||||
| raw := string(out) | |||||
| switch m.detectFunder(raw) { | |||||
| case Funder_AAA: | |||||
| e = m.AAA.decodeAAAPdf(raw) | |||||
| log.Println("AAA final result", m.AAA) | |||||
| break | |||||
| case Funder_Unknown: | |||||
| e = errors.New(Funder_Unknown) | |||||
| break // not able to detect Funder | |||||
| } | |||||
| return | |||||
| } | |||||
| func (m *AiDecodeIncome) detectFunder(raw string) FunderType { | |||||
| if m.isAAA(raw) { | |||||
| return Funder_AAA | |||||
| } | |||||
| return Funder_Unknown | |||||
| } | |||||
| func (m *AiDecodeIncome) isAAA(raw string) bool { | |||||
| keyword := "AAA Financial Trail Report" | |||||
| lines := strings.Split(raw, "\n") | |||||
| return m.checkFunderKeyword(keyword, lines, 0, 3) | |||||
| } | |||||
| func (m *AiDecodeIncome) checkFunderKeyword(keyword string, lines []string, start int, end int) bool { | |||||
| for idx, line := range lines { | |||||
| // first 10 lines has Key word | |||||
| if strings.Contains(line, keyword) && idx >= start && idx <= 10 { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } |
| package main | |||||
| import ( | |||||
| log "github.com/sirupsen/logrus" | |||||
| "testing" | |||||
| ) | |||||
| func TestDecodePayInMain(t *testing.T) { | |||||
| fileName := "./uploads/30.uploads" | |||||
| fileName = "/home/sp/go/src/SFM_Loan_RestApi/uploads/30.uploads" | |||||
| data, _ := decodePayInMain(fileName, "application/vnd.ms-excel") | |||||
| log.Println(data) | |||||
| } |
| package main | |||||
| import ( | |||||
| log "github.com/sirupsen/logrus" | |||||
| "strconv" | |||||
| "strings" | |||||
| "time" | |||||
| ) | |||||
| /* Sample Text | |||||
| AAA Financial Trail Report | |||||
| Super Finance Markets Pty Ltd | |||||
| Loan Number SettDate Loan Balance Arrears DisDate IntTrail$ Comments | |||||
| Facility | |||||
| Columbus | |||||
| Period Servicing: Feb 2020 | |||||
| 400053440 02-Sep-19 $552,463 552,579.52 $32.19 | |||||
| 400063271 19-Feb-20 $832,000 832,000.00 $0.00 | |||||
| Columbus Total: $32.19 | |||||
| Grand Total: $32.19 | |||||
| Super Finance Markets Pty Ltd | |||||
| */ | |||||
| type PayInAAARow struct { | |||||
| LoanNUmber string | |||||
| Settlement time.Time | |||||
| LoanAmount float64 | |||||
| Balance float64 | |||||
| InTrail float64 | |||||
| } | |||||
| type PayInAAAPeriod struct { | |||||
| Period time.Time | |||||
| Rows []PayInAAARow | |||||
| } | |||||
| type PayInAAAData struct { | |||||
| Periods []PayInAAAPeriod | |||||
| } | |||||
| func (m *PayInAAAData) decodeAAAPdf(raw string) (e error) { | |||||
| m.Periods = make([]PayInAAAPeriod, 0, 10) | |||||
| lines := strings.Split(raw, "\n") | |||||
| var tableHeader []string | |||||
| var tableHeaderLine int | |||||
| currentPeriod := -1 | |||||
| state := "start" | |||||
| for idx, l := range lines { // DFA, wow, finally it's used. after years of learning | |||||
| switch state { | |||||
| case "start": | |||||
| state = m.processStart(l) | |||||
| if state == "LookingForPeriod" { | |||||
| tableHeaderLine = idx | |||||
| tableHeader = strings.Split(l, " ") | |||||
| log.Println("Find table header", tableHeader, l, tableHeaderLine) | |||||
| } | |||||
| break | |||||
| case "LookingForPeriod": | |||||
| state = m.processPeriod(l) | |||||
| if state == "LookingForRows" { | |||||
| batch := PayInAAAPeriod{} | |||||
| m.Periods = append(m.Periods, batch) | |||||
| currentPeriod++ //move index to next , or 0 for a start | |||||
| m.Periods[currentPeriod].Period, e = m.getPeriod(l) | |||||
| m.Periods[currentPeriod].Rows = make([]PayInAAARow, 0, 10) | |||||
| if e != nil { | |||||
| log.Warn("cannot find period", l, e) | |||||
| state = "LookingForPeriod" | |||||
| } | |||||
| } | |||||
| break | |||||
| case "LookingForRows", "LookingForRowsSkipCurrent": | |||||
| nextState, row, valid := m.processRow(l) | |||||
| if valid { | |||||
| m.Periods[currentPeriod].Rows = append(m.Periods[currentPeriod].Rows, row) | |||||
| } | |||||
| state = nextState | |||||
| break | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| func (m *PayInAAAData) processStart(line string) (nextState string) { | |||||
| nextState = "start" | |||||
| if strings.Contains(line, "Loan Number") && | |||||
| strings.Contains(line, "SettDate") && | |||||
| strings.Contains(line, "Balance") && | |||||
| strings.Contains(line, "IntTrail$") { | |||||
| nextState = "LookingForPeriod" | |||||
| } | |||||
| return | |||||
| } | |||||
| func (m *PayInAAAData) processPeriod(line string) (nextState string) { | |||||
| nextState = "LookingForPeriod" | |||||
| if strings.Contains(line, "Period Servicing:") { | |||||
| nextState = "LookingForRows" | |||||
| } | |||||
| return | |||||
| } | |||||
| // Period Servicing: Feb 2020 | |||||
| func (m *PayInAAAData) getPeriod(line string) (p time.Time, e error) { | |||||
| idx := strings.Index(line, ":") | |||||
| subStr := strings.TrimSpace(line[idx+1:]) | |||||
| p, e = time.Parse("Jan 2006", subStr) | |||||
| return | |||||
| } | |||||
| func (m *PayInAAAData) processRow(line string) (nextState string, row PayInAAARow, valid bool) { | |||||
| nextState = "LookingForRows" | |||||
| valid = false | |||||
| allParts := strings.Split(line, " ") | |||||
| el := make([]string, 0, 10) | |||||
| for _, item := range allParts { | |||||
| if len(item) > 0 { | |||||
| el = append(el, item) | |||||
| } | |||||
| } | |||||
| if len(el) >= 5 { | |||||
| row.LoanNUmber = el[0] | |||||
| row.Settlement, _ = time.Parse("02-Jan-06", el[1]) | |||||
| row.LoanAmount = m.currencyToFloat64(el[2]) | |||||
| row.Balance = m.currencyToFloat64(el[3]) | |||||
| row.InTrail = m.currencyToFloat64(el[len(el)-1]) //last element | |||||
| valid = true | |||||
| } else { | |||||
| if strings.Contains(line, "Total:") { | |||||
| nextState = "start" | |||||
| } else { | |||||
| nextState = "LookingForRowsSkipCurrent" | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| func (m *PayInAAAData) currencyToFloat64(cur string) (ret float64) { | |||||
| cur = strings.ReplaceAll(cur, " ", "") //remove space | |||||
| cur = strings.ReplaceAll(cur, "$", "") //remove $ | |||||
| cur = strings.ReplaceAll(cur, ",", "") //remove , | |||||
| ret, _ = strconv.ParseFloat(cur, 64) | |||||
| return ret | |||||
| } |