commit 51f293c4d0d2ed8ab9f03004901c1ca31e80e228
Author: tms <nemi@skaut.cz>
Date: Thu, 8 Apr 2021 10:30:53 +0200
init commit
Diffstat:
A | .gitignore | | | 3 | +++ |
A | fio/api.go | | | 41 | +++++++++++++++++++++++++++++++++++++++++ |
A | fio/fio.go | | | 30 | ++++++++++++++++++++++++++++++ |
A | fio/model.go | | | 55 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | go.mod | | | 5 | +++++ |
A | go.sum | | | 10 | ++++++++++ |
A | main.go | | | 146 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | web/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>