gofio

http server for overview for accounts from FIO bank
git clone git://gtms.dev/gofio.git
Log | Files | Refs

commit 51f293c4d0d2ed8ab9f03004901c1ca31e80e228
Author: tms <nemi@skaut.cz>
Date:   Thu,  8 Apr 2021 10:30:53 +0200

init commit

Diffstat:
A.gitignore | 3+++
Afio/api.go | 41+++++++++++++++++++++++++++++++++++++++++
Afio/fio.go | 30++++++++++++++++++++++++++++++
Afio/model.go | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 5+++++
Ago.sum | 10++++++++++
Amain.go | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/index.html | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 356 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +token +t.json +gofio diff --git a/fio/api.go b/fio/api.go @@ -0,0 +1,41 @@ +package fio + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +func fmtDate(time time.Time) string { + return fmt.Sprintf("%d-%d-%d", time.Year(), time.Month(), time.Day()) +} + +func get(url string) *Root { + resp, err := http.Get(url) + if err != nil { + log.Fatal(err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + return nil + } + var root Root + err = json.Unmarshal(body, &root) + if err != nil { + log.Fatal(err) + return nil + } + + return &root + } + + return nil +} diff --git a/fio/fio.go b/fio/fio.go @@ -0,0 +1,30 @@ +package fio + +import ( + "fmt" + "time" +) + +type Fio struct { + Base_url string + Token string +} + +func (f *Fio) Period(from time.Time, to time.Time) *Root { + return get(fmt.Sprintf("%s/periods/%s/%s/%s/transactions.json", f.Base_url, f.Token, fmtDate(from), fmtDate(to))) +} + +func (f *Fio) Statement(year int, id int) *Root { + return get(fmt.Sprintf("%s/by_id/%s/%d/%d/transactions.json", f.Base_url, f.Token, year, id)) +} +func (f *Fio) Last() *Root { + return get(fmt.Sprintf("%s/last/%s/transactions.json", f.Base_url, f.Token)) +} + +func (f *Fio) SetLastID(id int) *Root { + return get(fmt.Sprintf("%s/set-last-id/%s/%d", f.Base_url, f.Token, id)) +} + +func (f *Fio) SetLastDate(date time.Time) *Root { + return get(fmt.Sprintf("%s/set-last-date/%s/%s", f.Base_url, f.Token, fmtDate(date))) +} diff --git a/fio/model.go b/fio/model.go @@ -0,0 +1,55 @@ +package fio + +type Root struct { + AccountStatement AccountStatement +} + +type AccountStatement struct { + Info *Info + TransactionList *TransactionList +} + +type Info struct { + AccountId string + BankId string + Currency string + Iban string + Bic string + OpeningBalance float64 + ClosingBalance float64 + DateStart string + DateEnd string + YearList int + IdList int64 + IdFrom int64 + IdTo int64 + IdLastDownload int64 +} + +type TransactionList struct { + Transactions []*Transaction `json:"Transaction"` +} + +type Transaction struct { + Uid *Column `json:"column22"` + Id *Column `json:"column17"` + Money *Column `json:"column1"` + Currency *Column `json:"column14"` + BankName *Column `json:"column12"` + BankCode *Column `json:"column3"` + Date *Column `json:"column0"` + BeneficiaryAccount *Column `json:"column2"` + BeneficiaryName *Column `json:"column10"` + BeneficiaryMessage *Column `json:"column16"` + User *Column `json:"column7"` + Type *Column `json:"column8"` + KS *Column `json:"column4,omitempty"` + VS *Column `json:"column5,omitempty"` + SS *Column `json:"column6,omitempty"` +} + +type Column struct { + Value interface{} + Name string + Id int +} diff --git a/go.mod b/go.mod @@ -0,0 +1,5 @@ +module fio + +go 1.16 + +require github.com/leekchan/accounting v1.0.0 diff --git a/go.sum b/go.sum @@ -0,0 +1,10 @@ +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/leekchan/accounting v1.0.0 h1:+Wd7dJ//dFPa28rc1hjyy+qzCbXPMR91Fb6F1VGTQHg= +github.com/leekchan/accounting v1.0.0/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= diff --git a/main.go b/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "bytes" + "fio/fio" + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/leekchan/accounting" +) + +const base_url = "https://www.fio.cz/ib_api/rest/" + +var token = flag.String("t", "", "fio api token") + +var page string + +func main() { + flag.Parse() + + if *token == "" { + log.Fatal("Token was not provided") + os.Exit(1) + } + + http.HandleFunc("/", serveFiles) + + log.Println("Listening on :3000...") + err := http.ListenAndServe(":3000", nil) + if err != nil { + log.Fatal(err) + os.Exit(1) + } +} +func serveFiles(w http.ResponseWriter, r *http.Request) { + index, _ := os.ReadFile("./web/index.html") + fio := &fio.Fio{Base_url: base_url, Token: *token} + + if page != "" && !hasNew(fio) { + log.Println("No new updates") + fmt.Fprint(w, page) + return + } + + root := data(fio) + if root == nil { + log.Println("Nil root") + fmt.Fprint(w, page) + return + } + log.Println("Serving new") + ac := accounting.Accounting{Symbol: root.AccountStatement.Info.Currency, Precision: 2, Thousand: " ", Format: "%v %s"} + // Templating + file := string(index) + file = strings.Replace(file, "{{ACC_ID}}", formatAccId(root), -1) + file = strings.Replace(file, "{{ACC_IBAN}}", root.AccountStatement.Info.Iban, -1) + file = strings.Replace(file, "{{ACC_BAL}}", fmt.Sprintf("%v", ac.FormatMoney(root.AccountStatement.Info.ClosingBalance)), -1) + file = strings.Replace(file, "{{LIST}}", formatList(root), -1) + // serving + page = string(file) + fmt.Fprint(w, page) +} + +func formatAccId(root *fio.Root) string { + return fmt.Sprintf("%s / %s", root.AccountStatement.Info.AccountId, root.AccountStatement.Info.BankId) +} + +func formatList(root *fio.Root) string { + ac := accounting.Accounting{Precision: 2, Thousand: " ", Format: "%v %s"} + headers := make(map[int]string) + var head bytes.Buffer + var rows bytes.Buffer + for _, v := range reverse(root.AccountStatement.TransactionList.Transactions) { + columns := []*fio.Column{ + v.Date, + v.Money, + v.BeneficiaryAccount, + v.BeneficiaryMessage, + v.Type, + } + + rows.WriteString("<tr>") + for _, c := range columns { + if c == nil { + rows.WriteString("<td></td>") + continue + } + if _, in := headers[c.Id]; !in { + headers[c.Id] = c.Name + head.WriteString(fmt.Sprintf("<th>%s</th>", c.Name)) + } + switch c.Id { + case 1: + moneyClass := "positive" + if c.Value.(float64) < 0 { + moneyClass = "negative" + } + ac.Symbol = v.Currency.Value.(string) + rows.WriteString(fmt.Sprintf("<td class='%s %s'>%v</td>", "money", moneyClass, ac.FormatMoney(c.Value))) + case 2: + rows.WriteString(fmt.Sprintf("<td><span>%v</span> / <span>%v</span></td>", c.Value, v.BankCode.Value)) + default: + rows.WriteString(fmt.Sprintf("<td>%v</td>", c.Value)) + } + } + rows.WriteString("</tr>") + + } + + return fmt.Sprintf(` + <thead> + %s + </thead> + <tbody> + %s + </tbody> +`, head.String(), rows.String()) +} + +func data(fio *fio.Fio) *fio.Root { + from := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC) + to := time.Date(2021, time.December, 31, 0, 0, 0, 0, time.UTC) + + return fio.Period(from, to) +} + +func hasNew(fio *fio.Fio) bool { + last := fio.Last() + if last == nil { + return false + } + return len(last.AccountStatement.TransactionList.Transactions) > 0 +} + +func reverse(numbers []*fio.Transaction) []*fio.Transaction { + for i := 0; i < len(numbers)/2; i++ { + j := len(numbers) - i - 1 + numbers[i], numbers[j] = numbers[j], numbers[i] + } + return numbers +} diff --git a/web/index.html b/web/index.html @@ -0,0 +1,66 @@ +<!doctype html> +<html> + +<head> + <meta charset="utf-8"> + <title>Milion</title> + <style> + table { + border-collapse: collapse; + width: 100%; + } + + table tr:hover td { + background-color: #e6e6e6; + } + + table.head tr td:first-child { + text-align: right + } + + table.head tr td:last-child { + text-align: left + } + + th, + td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; + } + + td.positive { + color: green; + } + + td.negative { + color: red; + } + + td.money { + text-align: right; + } + </style> +</head> + +<body> + <h1>Milion</h1> + <table class="head"> + <tbody> + <tr> + <td>Číslo účtu:</td> + <td>{{ACC_ID}}</td> + </tr> + <tr> + <td>IBAN:</td> + <td>{{ACC_IBAN}}</td> + </tr> + <tr> + <td>Disponibilní zůstatek:</td> + <td>{{ACC_BAL}}</td> + </tr> + </table> + <table>{{LIST}}</table> +</body> + +</html>