diff --git a/server/controllers/files.go b/server/controllers/files.go index 7d6ef3e..f67a86a 100644 --- a/server/controllers/files.go +++ b/server/controllers/files.go @@ -1,6 +1,7 @@ package controllers import ( + "io/ioutil" "net/http" "os" @@ -116,6 +117,18 @@ func getAttachmentFile(c *gin.Context) { } } +func getFileBytes(c *gin.Context, fileVariable string) ([]byte, error) { + if fileVariable == "" { + fileVariable = "file" + } + formFile, err := c.FormFile(fileVariable) + if err != nil { + return nil, err + } + openedFile, _ := formFile.Open() + return ioutil.ReadAll(openedFile) +} + func saveUploadedFile(c *gin.Context, fileVariable string) (*db.Attachment, error) { if fileVariable == "" { fileVariable = "file" diff --git a/server/controllers/import.go b/server/controllers/import.go new file mode 100644 index 0000000..746c38a --- /dev/null +++ b/server/controllers/import.go @@ -0,0 +1,26 @@ +package controllers + +import ( + "net/http" + + "github.com/akhilrex/hammond/service" + "github.com/gin-gonic/gin" +) + +func RegisteImportController(router *gin.RouterGroup) { + router.POST("/import/fuelly", fuellyImport) +} + +func fuellyImport(c *gin.Context) { + bytes, err := getFileBytes(c, "file") + if err != nil { + c.JSON(http.StatusUnprocessableEntity, err) + return + } + err = service.FuellyImport(bytes, c.MustGet("userId").(string)) + if err != nil { + c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{}) +} diff --git a/server/go.mod b/server/go.mod index 6be6abc..71194ef 100644 --- a/server/go.mod +++ b/server/go.mod @@ -10,6 +10,7 @@ require ( github.com/go-playground/validator/v10 v10.4.1 github.com/jasonlvhit/gocron v0.0.1 github.com/joho/godotenv v1.3.0 + github.com/leekchan/accounting v1.0.0 // indirect github.com/satori/go.uuid v1.2.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 diff --git a/server/go.sum b/server/go.sum index 6548b7c..49147b6 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,3 +1,5 @@ +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -45,8 +47,11 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leekchan/accounting v1.0.0 h1:+Wd7dJ//dFPa28rc1hjyy+qzCbXPMR91Fb6F1VGTQHg= +github.com/leekchan/accounting v1.0.0/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= @@ -58,10 +63,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= diff --git a/server/main.go b/server/main.go index 2aad465..bbb96d7 100644 --- a/server/main.go +++ b/server/main.go @@ -49,6 +49,7 @@ func main() { controllers.RegisterAuthController(router) controllers.RegisterVehicleController(router) controllers.RegisterFilesController(router) + controllers.RegisteImportController(router) go assetEnv() go intiCron() diff --git a/server/service/importService.go b/server/service/importService.go new file mode 100644 index 0000000..92ea4f3 --- /dev/null +++ b/server/service/importService.go @@ -0,0 +1,157 @@ +package service + +import ( + "bytes" + "encoding/csv" + "fmt" + "strconv" + "strings" + "time" + + "github.com/akhilrex/hammond/db" + "github.com/leekchan/accounting" +) + +func FuellyImport(content []byte, userId string) error { + stream := bytes.NewReader(content) + reader := csv.NewReader(stream) + records, err := reader.ReadAll() + + if err != nil { + return err + } + + vehicles, err := GetUserVehicles(userId) + if err != nil { + return err + } + user, err := GetUserById(userId) + + if err != nil { + return err + } + + var vehicleMap map[string]db.Vehicle = make(map[string]db.Vehicle) + for _, vehicle := range *vehicles { + vehicleMap[vehicle.Nickname] = vehicle + } + + var fillups []db.Fillup + var expenses []db.Expense + layout := "2006-01-02 15:04" + altLayout := "2006-01-02 3:04 PM" + + var errors []string + + for index, record := range records { + if index == 0 { + continue + } + + var vehicle db.Vehicle + var ok bool + if vehicle, ok = vehicleMap[record[4]]; !ok { + errors = append(errors, "Found an unmapped vehicle entry at row "+strconv.Itoa(index+1)) + } + dateStr := record[2] + " " + record[3] + date, err := time.Parse(layout, dateStr) + if err != nil { + date, err = time.Parse(altLayout, dateStr) + } + if err != nil { + errors = append(errors, "Found an invalid date/time at row "+strconv.Itoa(index+1)) + } + + totalCostStr := accounting.UnformatNumber(record[9], 3, user.Currency) + totalCost64, err := strconv.ParseFloat(totalCostStr, 32) + if err != nil { + errors = append(errors, "Found an invalid total cost at row "+strconv.Itoa(index+1)) + } + + totalCost := float32(totalCost64) + odoStr := accounting.UnformatNumber(record[5], 0, user.Currency) + odoreading, err := strconv.Atoi(odoStr) + if err != nil { + errors = append(errors, "Found an invalid odo reading at row "+strconv.Itoa(index+1)) + } + location := record[12] + + //Create Fillup + if record[0] == "Gas" { + rateStr := accounting.UnformatNumber(record[7], 3, user.Currency) + ratet64, err := strconv.ParseFloat(rateStr, 32) + if err != nil { + errors = append(errors, "Found an invalid cost per gallon at row "+strconv.Itoa(index+1)) + } + rate := float32(ratet64) + + quantity64, err := strconv.ParseFloat(record[8], 32) + if err != nil { + errors = append(errors, "Found an invalid quantity at row "+strconv.Itoa(index+1)) + } + quantity := float32(quantity64) + + notes := fmt.Sprintf("Octane:%s\nGas Brand:%s\nLocation%s\nTags:%s\nPayment Type:%s\nTire Pressure:%s\nNotes:%s\nMPG:%s", + record[10], record[11], record[12], record[13], record[14], record[15], record[16], record[1], + ) + + isTankFull := record[6] == "Full" + + fillups = append(fillups, db.Fillup{ + VehicleID: vehicle.ID, + FuelUnit: vehicle.FuelUnit, + FuelQuantity: quantity, + PerUnitPrice: rate, + TotalAmount: totalCost, + OdoReading: odoreading, + IsTankFull: &isTankFull, + Comments: notes, + FillingStation: location, + UserID: userId, + Date: date, + Currency: user.Currency, + DistanceUnit: user.DistanceUnit, + }) + + } + if record[0] == "Service" { + notes := fmt.Sprintf("Tags:%s\nPayment Type:%s\nNotes:%s", + record[13], record[14], record[16], + ) + expenses = append(expenses, db.Expense{ + VehicleID: vehicle.ID, + Amount: totalCost, + OdoReading: odoreading, + Comments: notes, + ExpenseType: record[17], + UserID: userId, + Currency: user.Currency, + Date: date, + DistanceUnit: user.DistanceUnit, + }) + } + + } + if len(errors) != 0 { + return fmt.Errorf(strings.Join(errors, "\n")) + } + + tx := db.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := tx.Error; err != nil { + return err + } + if err := tx.Create(&fillups).Error; err != nil { + tx.Rollback() + return err + } + if err := tx.Create(&expenses).Error; err != nil { + tx.Rollback() + return err + } + return tx.Commit().Error +}