Przeglądaj źródła

AAA decoding works.

master
sp 4 lat temu
rodzic
commit
ed619dcbae
7 zmienionych plików z 491 dodań i 3 usunięć
  1. +6
    -0
      apiV1Response.go
  2. +186
    -0
      apiV1Uploads.go
  3. +19
    -2
      apiv1.go
  4. +5
    -1
      debug.go
  5. +112
    -0
      pay-in-decode.go
  6. +13
    -0
      pay-in-decode_test.go
  7. +150
    -0
      payIn-AAA.go

+ 6
- 0
apiV1Response.go Wyświetl plik

@@ -54,6 +54,9 @@ func (m *apiV1Response) toJson() (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
m.tmp = nil
if config.Debug {
@@ -67,6 +70,9 @@ func (m *apiV1Response) sendJson(w http.ResponseWriter) (ret []byte, e error) {
}

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)
if e != nil {
log.Warn("Cannot convert result to json ", result)

+ 186
- 0
apiV1Uploads.go Wyświetl plik

@@ -0,0 +1,186 @@
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
}

+ 19
- 2
apiv1.go Wyświetl plik

@@ -70,6 +70,10 @@ func setupApiV1Handler() []apiV1HandlerMap {
{"DELETE", "payIn/", apiV1PayInDelete},
{"GET", "user-reward/", apiV1UserReward},
{"GET", "login-available/", apiV1LoginAvailable},

{"POST", "lender-upload/", apiV1UploadsPost},
{"GET", "lender-upload/", apiV1UploadsGet},

{"GET", "login", apiV1DumpRequest},
}
} else { //production
@@ -117,6 +121,10 @@ func setupApiV1Handler() []apiV1HandlerMap {
{"DELETE", "payIn/", apiV1PayInDelete},
{"GET", "user-reward/", apiV1UserReward},
{"GET", "login-available/", apiV1LoginAvailable},

{"POST", "lender-upload/", apiV1UploadsPost},
{"GET", "lender-upload/", apiV1UploadsGet},

{"GET", "login", apiV1EmptyResponse},
}
}
@@ -126,8 +134,6 @@ func setupApiV1Handler() []apiV1HandlerMap {
//apiV1Main version 1 main entry for all REST API
//
func apiV1Main(w http.ResponseWriter, r *http.Request) {
//general setup
w.Header().Set("Content-Type", "application/json;charset=UTF-8")

//CORS setup
setupCrossOriginResponse(&w, r)
@@ -293,6 +299,8 @@ func apiV1ErrorCheck(e error) {
}

func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
//general setup
w.Header().Set("Content-Type", "application/json;charset=UTF-8")

w.WriteHeader(500)
apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies
@@ -305,6 +313,9 @@ func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
}

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)
type struct403 struct {
Error int
@@ -324,6 +335,9 @@ func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Sessio
}

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)
type struct404 struct {
Error int
@@ -361,6 +375,9 @@ func apiV1DumpRequest(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
fmt.Fprintf(w, "")
}

+ 5
- 1
debug.go Wyświetl plik

@@ -7,7 +7,11 @@ import (

func logRequestDebug(data []byte, err error) (ret string) {
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)
} else {
log.Fatalf("%s\n\n", err)

+ 112
- 0
pay-in-decode.go Wyświetl plik

@@ -0,0 +1,112 @@
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
}

+ 13
- 0
pay-in-decode_test.go Wyświetl plik

@@ -0,0 +1,13 @@
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)
}

+ 150
- 0
payIn-AAA.go Wyświetl plik

@@ -0,0 +1,150 @@
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
}

Ładowanie…
Anuluj
Zapisz