| return "pdf", nil | return "pdf", nil | ||||
| } | } | ||||
| if strings.ToLower(strMime) == "application/vnd.ms-excel" { | |||||
| if strings.ToLower(strMime) == "application/vnd.ms-excel" || strings.ToLower(m.Upload.Format) == | |||||
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" { | |||||
| return "excel", nil | return "excel", nil | ||||
| } | } | ||||
| return "opensheet", nil | return "opensheet", nil | ||||
| } | } | ||||
| // check suffix which is not reliable | |||||
| ext := filepath.Ext(m.Upload.FileName) | |||||
| ext = strings.ToLower(ext) | |||||
| if ext[0] == '.' { | |||||
| ext = ext[1:] // remove the first char 'dot' . | |||||
| } | |||||
| switch ext { | |||||
| case "xls", "xlsx": | |||||
| return "excel", nil | |||||
| case "pdf": | |||||
| return "pdf", nil | |||||
| default: | |||||
| log.Warn("unhandled uploads type", ext) | |||||
| } | |||||
| return "", nil | return "", nil | ||||
| } | } | ||||
| _, name := filepath.Split("/home/sp/uploads/abc.uploads") | _, name := filepath.Split("/home/sp/uploads/abc.uploads") | ||||
| test := fileNameWithoutExtTrimSuffix(name) + ".jpg" | test := fileNameWithoutExtTrimSuffix(name) + ".jpg" | ||||
| log.Println(test) | log.Println(test) | ||||
| test1 := filepath.Ext("abc.uploads.pdf") | |||||
| log.Println(test1) | |||||
| ext := "." | |||||
| log.Println(ext[1:]) | |||||
| } | } |
| "time" | "time" | ||||
| ) | ) | ||||
| func apiV1UploadsGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| id := r.URL.Path[len(apiV1Prefix+"upload/"):] //remove prefix | |||||
| func apiV1UploadMetaGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| id := r.URL.Path[len(apiV1Prefix+"upload-meta/"):] //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 | |||||
| } | |||||
| ulmeta := loan.Uploads{} | |||||
| e = ulmeta.Read(int64(intId)) | |||||
| if e != nil { | |||||
| log.Println("upload not found", id, e) | |||||
| apiV1Client404Error(w, r, ss) // bad request | |||||
| return | |||||
| } | |||||
| apiV1SendJson(ulmeta, w, r, ss) | |||||
| } | |||||
| func apiV1UploadOriginalFileGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| id := r.URL.Path[len(apiV1Prefix+"upload-original/"):] //remove prefix | |||||
| intId, e := strconv.Atoi(id) | intId, e := strconv.Atoi(id) | ||||
| if e != nil { | if e != nil { | ||||
| log.Println("invalid id for upload get", id, e) | log.Println("invalid id for upload get", id, e) | ||||
| apiV1Server500Error(w, r) // bad request | apiV1Server500Error(w, r) // bad request | ||||
| return | return | ||||
| } | } | ||||
| ai := AiDecodeIncome{} | |||||
| ai.decodeUploadToPayIn(ul.Upload) | |||||
| apiV1SendJson(ai, w, r, ss) | |||||
| apiV1SendJson(ulMeta, w, r, ss) | |||||
| } | } | ||||
| func saveUploadsMetaToDB(id int64, filePath string, | func saveUploadsMetaToDB(id int64, filePath string, | ||||
| return out.Name(), e | return out.Name(), e | ||||
| } | } | ||||
| func getRequestedUpload(w http.ResponseWriter, r *http.Request, ss *loan.Session) (ret uploadsOnDisk, e error) { | |||||
| strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix | |||||
| func getRequestedUpload(strId string, w http.ResponseWriter, r *http.Request, ss *loan.Session) (ret uploadsOnDisk, e error) { | |||||
| Id, e := strconv.Atoi(strId) | Id, e := strconv.Atoi(strId) | ||||
| if e != nil { | if e != nil { | ||||
| log.Error("Invalid uploads Id cannot convert to integer", Id, e) | log.Error("Invalid uploads Id cannot convert to integer", Id, e) | ||||
| } | } | ||||
| func apiV1UploadAsImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1UploadAsImage(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| ul, e := getRequestedUpload(w, r, ss) | |||||
| strId := r.URL.Path[len(apiV1Prefix+"upload-as-image/"):] //remove prefix | |||||
| if strId == "default" { | |||||
| http.ServeFile(w, r, config.UploadsDir.JpgDefault) | |||||
| return | |||||
| } | |||||
| ul, e := getRequestedUpload(strId, w, r, ss) | |||||
| if e != nil { | if e != nil { | ||||
| return | return | ||||
| } | } | ||||
| defer f.Close() | defer f.Close() | ||||
| fi, e := f.Stat() | fi, e := f.Stat() | ||||
| if e == nil { | if e == nil { | ||||
| w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) | |||||
| //w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) | |||||
| http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) | http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) | ||||
| return | return | ||||
| } | } | ||||
| } | } | ||||
| func apiV1UploadAsThumbnail(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | func apiV1UploadAsThumbnail(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | ||||
| ul, e := getRequestedUpload(w, r, ss) | |||||
| strId := r.URL.Path[len(apiV1Prefix+"upload-as-thumbnail/"):] //remove prefix | |||||
| if strId == "default" { | |||||
| http.ServeFile(w, r, config.UploadsDir.ThumbDefault) | |||||
| return | |||||
| } | |||||
| ul, e := getRequestedUpload(strId, w, r, ss) | |||||
| if e != nil { | if e != nil { | ||||
| return | return | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| func apiV1UploadAPDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| ul, e := getRequestedUpload(w, r, ss) | |||||
| func apiV1UploadAsPDF(w http.ResponseWriter, r *http.Request, ss *loan.Session) { | |||||
| strId := r.URL.Path[len(apiV1Prefix+"upload-as-pdf/"):] //remove prefix | |||||
| if strId == "default" { | |||||
| http.ServeFile(w, r, config.UploadsDir.PdfDefault) | |||||
| return | |||||
| } | |||||
| ul, e := getRequestedUpload(strId, w, r, ss) | |||||
| if e != nil { | if e != nil { | ||||
| return | return | ||||
| } | } | ||||
| defer f.Close() | defer f.Close() | ||||
| fi, e := f.Stat() | fi, e := f.Stat() | ||||
| if e == nil { | if e == nil { | ||||
| w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) | |||||
| if forceHttpDownload(r) { | |||||
| w.Header().Set("Content-Disposition", "attachment; filename="+ul.Upload.FileName) | |||||
| } | |||||
| http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) | http.ServeContent(w, r, ul.filePath(), fi.ModTime(), f) | ||||
| return | return | ||||
| } | } | ||||
| apiV1Client404Error(w, r, ss) | apiV1Client404Error(w, r, ss) | ||||
| } | } | ||||
| } | } | ||||
| func forceHttpDownload(r *http.Request) bool { | |||||
| keys, ok := r.URL.Query()["download"] | |||||
| if !ok || len(keys[0]) < 1 { | |||||
| return false | |||||
| } | |||||
| key := keys[0] | |||||
| return key == "force" | |||||
| } |
| {"GET", "login-available/", apiV1LoginAvailable}, | {"GET", "login-available/", apiV1LoginAvailable}, | ||||
| {"POST", "lender-upload/", apiV1UploadsPost}, | {"POST", "lender-upload/", apiV1UploadsPost}, | ||||
| {"GET", "lender-upload/", apiV1UploadsGet}, | |||||
| {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, | |||||
| {"GET", "upload-analysis/", apiV1UploadAnalysis}, | |||||
| {"GET", "upload-as-image/", apiV1UploadAsImage}, | |||||
| {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, | {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, | ||||
| {"GET", "upload-as-pdf/", apiV1UploadAPDF}, | |||||
| {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, | |||||
| {"GET", "upload-original/", apiV1UploadOriginalFileGet}, | |||||
| {"GET", "upload/", apiV1UploadMetaGet}, | |||||
| {"GET", "login", apiV1DumpRequest}, | {"GET", "login", apiV1DumpRequest}, | ||||
| } | } | ||||
| {"GET", "login-available/", apiV1LoginAvailable}, | {"GET", "login-available/", apiV1LoginAvailable}, | ||||
| {"POST", "lender-upload/", apiV1UploadsPost}, | {"POST", "lender-upload/", apiV1UploadsPost}, | ||||
| {"GET", "lender-upload/", apiV1UploadsGet}, | |||||
| {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, | |||||
| {"GET", "upload-analysis/", apiV1UploadAnalysis}, | {"GET", "upload-analysis/", apiV1UploadAnalysis}, | ||||
| {"GET", "upload-as-image/", apiV1UploadAsImage}, | {"GET", "upload-as-image/", apiV1UploadAsImage}, | ||||
| {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, | {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail}, | ||||
| {"GET", "upload-as-pdf/", apiV1UploadAPDF}, | |||||
| {"GET", "upload/", apiV1UploadsGet}, | |||||
| {"GET", "upload-as-pdf/", apiV1UploadAsPDF}, | |||||
| {"GET", "upload-original/", apiV1UploadOriginalFileGet}, | |||||
| {"GET", "upload-meta/", apiV1UploadMetaGet}, | |||||
| {"GET", "login", apiV1EmptyResponse}, | {"GET", "login", apiV1EmptyResponse}, | ||||
| } | } |
| ) | ) | ||||
| type AiDecodeIncome struct { | type AiDecodeIncome struct { | ||||
| Id int64 | |||||
| Input loan.Uploads | Input loan.Uploads | ||||
| ul uploadsOnDisk // internal data | ul uploadsOnDisk // internal data | ||||
| Mime string //mime actually detected. | Mime string //mime actually detected. | ||||
| PayIn []loan.PayIn | PayIn []loan.PayIn | ||||
| Funder FunderType | Funder FunderType | ||||
| AAA PayInAAAData | |||||
| AAA []PayInAAAPeriod | |||||
| } | } | ||||
| func (m *AiDecodeIncome) decodeUploadToPayIn(ulMeta loan.Uploads) (e error) { | func (m *AiDecodeIncome) decodeUploadToPayIn(ulMeta loan.Uploads) (e error) { | ||||
| m.Id = ulMeta.Id | |||||
| m.Input = ulMeta | m.Input = ulMeta | ||||
| m.ul.Upload = ulMeta | m.ul.Upload = ulMeta | ||||
| m.PayIn = make([]loan.PayIn, 0, 10) | m.PayIn = make([]loan.PayIn, 0, 10) | ||||
| switch m.getFileType() { | switch m.getFileType() { | ||||
| case "pdf": | case "pdf": | ||||
| m.decodePdf() | m.decodePdf() | ||||
| switch m.detectFunder(raw) { | switch m.detectFunder(raw) { | ||||
| case Funder_AAA: | case Funder_AAA: | ||||
| m.Funder = Funder_AAA | m.Funder = Funder_AAA | ||||
| e = m.AAA.decodeAAAPdf(raw) | |||||
| e = m.decodeAAAPdf(raw) | |||||
| log.Println("AAA final result", m.AAA) | log.Println("AAA final result", m.AAA) | ||||
| break | break | ||||
| case Funder_Unknown: | case Funder_Unknown: |
| Rows []PayInAAARow | Rows []PayInAAARow | ||||
| } | } | ||||
| type PayInAAAData struct { | |||||
| Periods []PayInAAAPeriod | |||||
| } | |||||
| func (m *PayInAAAData) decodeAAAPdf(raw string) (e error) { | |||||
| m.Periods = make([]PayInAAAPeriod, 0, 10) | |||||
| func (m *AiDecodeIncome) decodeAAAPdf(raw string) (e error) { | |||||
| m.AAA = make([]PayInAAAPeriod, 0, 10) | |||||
| lines := strings.Split(raw, "\n") | lines := strings.Split(raw, "\n") | ||||
| currentPeriod := -1 | |||||
| currentDecoder := PayInAAAPeriod{} | |||||
| state := "start" | state := "start" | ||||
| for _, l := range lines { // DFA, wow, finally it's used. after years of learning | for _, l := range lines { // DFA, wow, finally it's used. after years of learning | ||||
| switch state { | switch state { | ||||
| case "start": | case "start": | ||||
| state = m.processStart(l) | |||||
| state = currentDecoder.processStart(l) | |||||
| if state == "LookingForPeriod" { | if state == "LookingForPeriod" { | ||||
| // determine column index, if their column is changing | // determine column index, if their column is changing | ||||
| } | } | ||||
| break | break | ||||
| case "LookingForPeriod": | case "LookingForPeriod": | ||||
| state = m.processPeriod(l) | |||||
| state = currentDecoder.processPeriod(l) | |||||
| if state == "LookingForRows" { | 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) | |||||
| currentDecoder.Period, e = currentDecoder.getPeriod(l) | |||||
| currentDecoder.Rows = make([]PayInAAARow, 0, 10) | |||||
| if e != nil { | if e != nil { | ||||
| log.Warn("cannot find period", l, e) | log.Warn("cannot find period", l, e) | ||||
| state = "LookingForPeriod" | state = "LookingForPeriod" | ||||
| } else { | |||||
| m.AAA = append(m.AAA, currentDecoder) | |||||
| } | } | ||||
| } | } | ||||
| break | break | ||||
| case "LookingForRows", "LookingForRowsSkipCurrent": | case "LookingForRows", "LookingForRowsSkipCurrent": | ||||
| nextState, row, valid := m.processRow(l) | |||||
| nextState, row, valid := currentDecoder.processRow(l) | |||||
| if valid { | if valid { | ||||
| m.Periods[currentPeriod].Rows = append(m.Periods[currentPeriod].Rows, row) | |||||
| currentDecoder.Rows = append(currentDecoder.Rows, row) | |||||
| } | } | ||||
| state = nextState | state = nextState | ||||
| if nextState == "start" { | |||||
| currentDecoder = PayInAAAPeriod{} //renew to a empty state | |||||
| } | |||||
| break | break | ||||
| } | } | ||||
| } | } | ||||
| return | return | ||||
| } | } | ||||
| func (m *PayInAAAData) processStart(line string) (nextState string) { | |||||
| func (m *PayInAAAPeriod) processStart(line string) (nextState string) { | |||||
| nextState = "start" | nextState = "start" | ||||
| if strings.Contains(line, "Loan Number") && | if strings.Contains(line, "Loan Number") && | ||||
| strings.Contains(line, "SettDate") && | strings.Contains(line, "SettDate") && | ||||
| return | return | ||||
| } | } | ||||
| func (m *PayInAAAData) processPeriod(line string) (nextState string) { | |||||
| func (m *PayInAAAPeriod) processPeriod(line string) (nextState string) { | |||||
| nextState = "LookingForPeriod" | nextState = "LookingForPeriod" | ||||
| if strings.Contains(line, "Period Servicing:") { | if strings.Contains(line, "Period Servicing:") { | ||||
| nextState = "LookingForRows" | nextState = "LookingForRows" | ||||
| } | } | ||||
| // Period Servicing: Feb 2020 | // Period Servicing: Feb 2020 | ||||
| func (m *PayInAAAData) getPeriod(line string) (p time.Time, e error) { | |||||
| func (m *PayInAAAPeriod) getPeriod(line string) (p time.Time, e error) { | |||||
| idx := strings.Index(line, ":") | idx := strings.Index(line, ":") | ||||
| subStr := strings.TrimSpace(line[idx+1:]) | subStr := strings.TrimSpace(line[idx+1:]) | ||||
| p, e = time.Parse("Jan 2006", subStr) | p, e = time.Parse("Jan 2006", subStr) | ||||
| return | return | ||||
| } | } | ||||
| func (m *PayInAAAData) processRow(line string) (nextState string, row PayInAAARow, valid bool) { | |||||
| func (m *PayInAAAPeriod) processRow(line string) (nextState string, row PayInAAARow, valid bool) { | |||||
| nextState = "LookingForRows" | nextState = "LookingForRows" | ||||
| valid = false | valid = false | ||||
| allParts := strings.Split(line, " ") | allParts := strings.Split(line, " ") | ||||
| return | return | ||||
| } | } | ||||
| func (m *PayInAAAData) currencyToFloat64(cur string) (ret float64) { | |||||
| func (m *PayInAAAPeriod) currencyToFloat64(cur string) (ret float64) { | |||||
| cur = strings.ReplaceAll(cur, " ", "") //remove space | cur = strings.ReplaceAll(cur, " ", "") //remove space | ||||
| cur = strings.ReplaceAll(cur, "$", "") //remove $ | cur = strings.ReplaceAll(cur, "$", "") //remove $ | ||||
| cur = strings.ReplaceAll(cur, ",", "") //remove , | cur = strings.ReplaceAll(cur, ",", "") //remove , |