From c2bed4bb09ed0cd6e2e80e3001d0d37689076e94 Mon Sep 17 00:00:00 2001 From: Volodymyr Kopytsia Date: Thu, 10 Aug 2023 22:11:28 +0300 Subject: [PATCH] [report]: add daily report --- .env.dev | 2 + .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- Dockerfile | 2 +- README.md | 3 + Resources/screenshot1.png | Bin 0 -> 33743 bytes app.go | 14 +++ bot.go | 57 ++++++----- client.go | 106 +++++--------------- docker-compose.yml | 7 +- go.mod | 10 ++ go.sum | 24 +++++ mono.go | 128 ++++++++++++++++++++++++ report.go | 33 +++--- schedule_report.go | 40 ++++++++ schedule_report_data.go | 183 ++++++++++++++++++++++++++++++++++ template.go | 13 ++- tools.go | 48 +++++++-- tools_test.go | 33 +++++- 19 files changed, 573 insertions(+), 134 deletions(-) create mode 100644 Resources/screenshot1.png create mode 100644 mono.go create mode 100644 schedule_report.go create mode 100644 schedule_report_data.go diff --git a/.env.dev b/.env.dev index 81ca400..cdaf366 100644 --- a/.env.dev +++ b/.env.dev @@ -7,6 +7,8 @@ TELEGRAM_TOKEN= MONO_TOKENS= TELEGRAM_ADMINS= TELEGRAM_CHATS= +# +SCHEDULE_TIME= 0 21 * * * # More info https://github.com/rs/zerolog#leveled-logging LOG_LEVEL=info diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 186b14e..96891ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.18.x + go-version: 1.21.x - uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 663173a..58466c5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.18.x + go-version: 1.21.x - uses: actions/checkout@v3 - uses: actions/cache@v3 diff --git a/Dockerfile b/Dockerfile index faba789..69a874b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # builder -FROM golang:1.18-alpine as builder +FROM golang:1.21-alpine as builder WORKDIR / diff --git a/README.md b/README.md index 6e65b5c..c1dd50a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ A simple telegram bot, written in Go with the [telegram-bot-api](https://github. ![mono_personal_tgbot](Resources/screenshot.png) +![mono_personal_tgbot](Resources/screenshot1.png) + ## Usage Run `mono_personal_tgbot` execution file in your terminal with following env variables @@ -19,6 +21,7 @@ Run `mono_personal_tgbot` execution file in your terminal with following env var `TELEGRAM_TOKEN` | [How to get telegram bot token](https://core.telegram.org/bots#3-how-do-i-create-a-bot) `TELEGRAM_ADMINS` | ids of the trusted user, example: `1234567,1234567` `TELEGRAM_CHATS` | ids of the trusted chats, example: `-1234567,-1234567` +`SCHEDULE_TIME` | set time for daily report, example: `0 21 * * *` `MONO_TOKENS` | [How to get monobank token](https://api.monobank.ua/) ### Telegram commands diff --git a/Resources/screenshot1.png b/Resources/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..90f94b9e543d5b37e9d5fd76fafd8c0e165719fd GIT binary patch literal 33743 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%0DN*`H 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

c&%;)j(Ji`C6J{ez|ve{owRqbr|N5 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 0 { - url = fmt.Sprintf("%s/%d", url, to) - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Error().Err(err).Msg("[monoapi] statements, NewRequest") - return statementItems, err - } - - req.Header.Add("x-token", c.token) - - return DoRequest(statementItems, req) + return c.mono.GetStatement(command, accountId, c.token) } -func (c client) getClientInfo() (ClientInfo, error) { - var clientInfo ClientInfo - - url := "https://api.monobank.ua/personal/client-info" - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Error().Err(err).Msg("[monoapi] client info, create request") - return clientInfo, err - } - - req.Header.Add("x-token", c.token) - - return DoRequest(clientInfo, req) +func (c Account) GetName() string { + return fmt.Sprintf("%s %s", c.Type, GetCurrencySymbol(c.CurrencyCode)) } diff --git a/docker-compose.yml b/docker-compose.yml index 74505ed..44726bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,9 +10,14 @@ services: dockerfile: Dockerfile environment: - TELEGRAM_TOKEN=${TELEGRAM_TOKEN} - - MONO_TOKEN=${MONO_TOKEN} + - MONO_TOKENS=${MONO_TOKENS} - TELEGRAM_ADMINS=${TELEGRAM_ADMINS} - TELEGRAM_CHATS=${TELEGRAM_CHATS} - LOG_LEVEL=${LOG_LEVEL} + - SCHEDULE_TIME=${SCHEDULE_TIME} ports: - ${APP_PORT}:8080 + logging: + options: + max-file: 5 + max-size: 15m diff --git a/go.mod b/go.mod index f2db7b9..edc9472 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,16 @@ require ( golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 ) +require ( + github.com/adhocore/gronx v1.6.5 // indirect + github.com/agiledragon/gomonkey/v2 v2.10.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/stretchr/testify v1.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index ff4a832..07c6b80 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,50 @@ +github.com/adhocore/gronx v1.6.5 h1:/pryEagBKz3WqUgpgvtL51eBN2rJLXowuW7rpS+jrew= +github.com/adhocore/gronx v1.6.5/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= +github.com/agiledragon/gomonkey/v2 v2.10.1 h1:FPJJNykD1957cZlGhr9X0zjr291/lbazoZ/dmc4mS4c= +github.com/agiledragon/gomonkey/v2 v2.10.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snabb/isoweek v1.0.1 h1:B4IsN2GU8lCNVkaUUgOzaVpPkKC2DdY9zcnxz5yc0qg= github.com/snabb/isoweek v1.0.1/go.mod h1:CAijAxH7NMgjqGc9baHMDE4sTHMt4B/f6X/XLiEE1iA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 h1:x03zeu7B2B11ySp+daztnwM5oBJ/8wGUSqrwcw9L0RA= golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mono.go b/mono.go new file mode 100644 index 0000000..92070c2 --- /dev/null +++ b/mono.go @@ -0,0 +1,128 @@ +package main + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/rs/zerolog/log" + "golang.org/x/time/rate" +) + +type Currency struct { + CurrencyCodeA int `json:"currencyCodeA"` + CurrencyCodeB int `json:"currencyCodeB"` + Date int `json:"date"` + RateBuy float64 `json:"rateBuy"` + RateCross float64 `json:"rateCross"` + RateSell float64 `json:"rateSell"` +} + +type Currencies []Currency + +type Mono struct { + limiter *rate.Limiter + limiter2 *rate.Limiter + limiter3 *rate.Limiter + limiter4 *rate.Limiter +} + +// NewMono returns a mono object. +func NewMono() *Mono { + return &Mono{ + limiter: rate.NewLimiter(rate.Every(time.Second*60), 1), + limiter2: rate.NewLimiter(rate.Every(time.Second*60), 1), + limiter3: rate.NewLimiter(rate.Every(time.Second*60), 1), + limiter4: rate.NewLimiter(rate.Every(time.Second*60), 1), + } +} + +// SetWebHook is a function set up the monobank webhook. +func (c Mono) SetWebHook(url, token string) (WebHookResponse, error) { + if !c.limiter.Allow() { + time.Sleep(61 * time.Second) + } + + response := WebHookResponse{} + + payload := strings.NewReader(fmt.Sprintf("{\"webHookUrl\": \"%s\"}", url)) + + req, err := http.NewRequest("POST", "https://api.monobank.ua/personal/webhook", payload) + if err != nil { + log.Error().Err(err).Msg("[monoapi] webhook, NewRequest") + return response, err + } + + req.Header.Add("X-Token", token) + req.Header.Add("content-type", "application/json") + + return DoRequest(response, req) +} + +func (c Mono) GetStatement(command, account string, token string) ([]StatementItem, error) { + if !c.limiter2.Allow() { + time.Sleep(61 * time.Second) + } + + statementItems := []StatementItem{} + + from, to, err := getTimeRangeByPeriod(command) + if err != nil { + log.Error().Err(err).Msg("[monoapi] statements, range") + return statementItems, err + } + + log.Debug().Msgf("[monoapi] statements, range from: %d, to: %d", from, to) + + url := fmt.Sprintf("https://api.monobank.ua/personal/statement/%s/%d", account, from) + if to > 0 { + url = fmt.Sprintf("%s/%d", url, to) + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Error().Err(err).Msg("[monoapi] statements, NewRequest") + return statementItems, err + } + + req.Header.Add("x-token", token) + + return DoRequest(statementItems, req) +} + +func (c Mono) GetClientInfo(token string) (ClientInfo, error) { + if !c.limiter3.Allow() { + time.Sleep(61 * time.Second) + } + + var clientInfo ClientInfo + + url := "https://api.monobank.ua/personal/client-info" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Error().Err(err).Msg("[monoapi] client info, create request") + return clientInfo, err + } + + req.Header.Add("x-token", token) + + return DoRequest(clientInfo, req) +} + +func (c Mono) GetCurrencies() (Currencies, error) { + if !c.limiter4.Allow() { + time.Sleep(61 * time.Second) + } + + var currencies Currencies + + url := "https://api.monobank.ua/bank/currency" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Error().Err(err).Msg("[monoapi] currency") + return currencies, err + } + + return DoRequest(currencies, req) +} diff --git a/report.go b/report.go index e35d868..c240a7d 100644 --- a/report.go +++ b/report.go @@ -52,11 +52,11 @@ type Report interface { type report struct { cache map[string][]StatementItem - prefix string - perPage int - tmpl *template.Template - accountId string - clientId uint32 + prefix string + perPage int + tmpl *template.Template + account *Account + clientId uint32 } // ReportPage is a structure to render report content the telegram @@ -70,7 +70,7 @@ type ReportPage struct { } // NewReport returns a report object. -func NewReport(accountId string, clientId uint32) Report { +func NewReport(account *Account, clientId uint32) Report { tmpl, err := GetTempate(reportPageTemplate) if err != nil { @@ -78,12 +78,12 @@ func NewReport(accountId string, clientId uint32) Report { } return &report{ - prefix: "rr", - perPage: 5, - cache: map[string][]StatementItem{}, - tmpl: tmpl, - accountId: accountId, - clientId: clientId, + prefix: "rr", + perPage: 5, + cache: map[string][]StatementItem{}, + tmpl: tmpl, + account: account, + clientId: clientId, } } @@ -196,14 +196,13 @@ func (r report) buildReportPage(items []StatementItem, page, limit int) ReportPa var amountTotal int var cashbackAmountTotal int var spentTotal int - var currencyCode int + for _, item := range items { if item.Amount < 0 { spentTotal += -item.Amount } amountTotal += abs(item.Amount) cashbackAmountTotal += item.CashbackAmount - currencyCode = item.CurrencyCode } if total > 0 { @@ -219,7 +218,7 @@ func (r report) buildReportPage(items []StatementItem, page, limit int) ReportPa return ReportPage{ StatementItems: items, AmountTotal: amountTotal, - CurrencyCode: currencyCode, + CurrencyCode: r.account.CurrencyCode, SpentTotal: spentTotal, CashbackAmountTotal: cashbackAmountTotal, } @@ -248,7 +247,7 @@ func (r report) GetKeyboarButtonConfig(update tgbotapi.Update, clientID uint32) ChatID: tgMessage.Chat.ID, FromID: tgMessage.From.ID, ClientID: r.clientId, - Account: r.accountId, + Account: r.account.ID, }) // add page number @@ -378,5 +377,5 @@ func (r *report) ResetLastData() { } func (r *report) IsAccount(accountId string) bool { - return r.accountId == accountId + return r.account.ID == accountId } diff --git a/schedule_report.go b/schedule_report.go new file mode 100644 index 0000000..7584cc0 --- /dev/null +++ b/schedule_report.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "errors" + + "github.com/adhocore/gronx" + "github.com/adhocore/gronx/pkg/tasker" +) + +type ScheduleReport struct { + cron *gronx.Gronx + Taskr *tasker.Tasker + scheduleTime string +} + +func NewScheduleReport(scheduleTime string) (*ScheduleReport, error) { + gron := gronx.New() + if !gron.IsValid(scheduleTime) { + return nil, errors.New("incorrect expression") + } + + taskr := tasker.New(tasker.Option{ + Verbose: true, + Tz: "Europe/Kyiv", + }) + + return &ScheduleReport{ + cron: &gron, + Taskr: taskr, + scheduleTime: scheduleTime, + }, nil +} + +func (s *ScheduleReport) Start(f func(ctx context.Context) (int, error)) { + // add task to run every minute + s.Taskr.Task(s.scheduleTime, f) + + s.Taskr.Run() +} diff --git a/schedule_report_data.go b/schedule_report_data.go new file mode 100644 index 0000000..7695a6c --- /dev/null +++ b/schedule_report_data.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "context" + "fmt" + + "github.com/rs/zerolog/log" + "github.com/samber/lo" +) + +type ScheduleReportData struct { + ClientInfo ClientInfo + StatementItems []StatementItem + Sum int + CashbackSum int + Count int + Currencies Currencies +} + +func (s *ScheduleReportData) IsEmpty() bool { + return s.StatementItems == nil || len(s.StatementItems) == 0 +} + +func (s *ScheduleReportData) Prepare() { + if s.IsEmpty() { + return + } + + _, ignoreAll := s.filterAndReduceStatements() + statements := s.filterStatements(ignoreAll) + + s.Count = len(statements) + s.Sum = s.calculateSum(statements) + s.CashbackSum = s.calculateCashbackSum(statements) +} + +func (s *ScheduleReportData) filterAndReduceStatements() (map[string]string, map[string]bool) { + fromStatements := s.filter4829Statements() + fromStatementsMap := s.reduceStatements(fromStatements) + + ignoreAll := map[string]bool{} + _ = s.filterOutFromAccounts(fromStatementsMap, ignoreAll) + + return fromStatementsMap, ignoreAll +} + +func (s *ScheduleReportData) filter4829Statements() []StatementItem { + return lo.Filter[StatementItem](s.StatementItems, func(item StatementItem, index int) bool { + return item.Mcc == 4829 && item.OriginalMcc == 4829 && item.Amount < 0 && item.OperationAmount < 0 && item.Amount != item.OperationAmount + }) +} + +func (s *ScheduleReportData) reduceStatements(fromStatements []StatementItem) map[string]string { + return lo.Reduce[StatementItem, map[string]string](fromStatements, func(agg map[string]string, item StatementItem, index int) map[string]string { + agg[fmt.Sprintf("%d %d %d %d", item.Mcc, item.OriginalMcc, -item.Amount, -item.OperationAmount)] = item.ID + return agg + }, map[string]string{}) +} + +func (s *ScheduleReportData) filterOutFromAccounts(fromStatementsMap map[string]string, ignoreAll map[string]bool) []StatementItem { + return lo.Filter[StatementItem](s.StatementItems, func(item StatementItem, index int) bool { + key := fmt.Sprintf("%d %d %d %d", item.Mcc, item.OriginalMcc, item.OperationAmount, item.Amount) + + id, ok := fromStatementsMap[key] + if ok { + ignoreAll[item.ID] = true + ignoreAll[id] = true + } + return !ok + }) +} + +func (s *ScheduleReportData) filterStatements(ignoreAll map[string]bool) []StatementItem { + return lo.Filter[StatementItem](s.StatementItems, func(item StatementItem, index int) bool { + _, ok := ignoreAll[item.ID] + return !ok + }) +} + +func (s *ScheduleReportData) calculateSum(statements []StatementItem) int { + return lo.Reduce[StatementItem, int](statements, func(agg int, item StatementItem, index int) int { + if item.Amount < 0 { + agg += s.calculateAmount(item) + } + return agg + }, 0) +} + +func (s *ScheduleReportData) calculateAmount(item StatementItem) int { + if item.Amount == item.OperationAmount && item.CurrencyCode != 980 { + return s.calculateNonUAHAmount(item) + } else if item.Amount != item.OperationAmount && item.CurrencyCode == 980 { + return -item.OperationAmount + } + return -item.Amount +} + +func (s *ScheduleReportData) calculateNonUAHAmount(item StatementItem) int { + currency, ok := lo.Find[Currency](s.Currencies, func(citem Currency) bool { + return citem.CurrencyCodeA == item.CurrencyCode && citem.CurrencyCodeB == 980 + }) + if ok { + return -item.Amount * (currency.CurrencyCodeA * 100) + } + return 0 +} + +func (s *ScheduleReportData) calculateCashbackSum(statements []StatementItem) int { + return lo.Reduce[StatementItem, int](statements, func(agg int, item StatementItem, index int) int { + agg = agg + item.CashbackAmount + return agg + }, 0) +} + +func (b *bot) ScheduleReport(ctx context.Context) (int, error) { + if len(b.clients) == 0 { + return 0, nil + } + + tmpl, err := GetTempate(scheduleReportTemplate) + if err != nil { + log.Fatal().Err(err).Msg("[template] error") + } + + currencies, err := b.mono.GetCurrencies() + if err != nil { + log.Err(err) + } + + for _, client := range b.clients { + scheduleReportData := ScheduleReportData{ + StatementItems: []StatementItem{}, + Currencies: currencies, + } + + info, err := client.GetInfo() + if err != nil { + log.Err(err) + continue + } + + scheduleReportData.ClientInfo = info + + for _, account := range info.Accounts { + items, err := client.GetStatement("Today", account.ID) + if err != nil { + log.Error().Err(err).Msg("[monobank] report, get statements") + continue + } + scheduleReportData.StatementItems = append(scheduleReportData.StatementItems, items...) + } + + if scheduleReportData.IsEmpty() { + log.Info().Msg("[monobank] schedule report") + continue + } + + scheduleReportData.Prepare() + + if scheduleReportData.Count == 0 || scheduleReportData.Sum == 0 { + log.Info().Msg("[monobank] schedule report, empty after filter") + continue + } + + var tpl bytes.Buffer + err = tmpl.Execute(&tpl, scheduleReportData) + if err != nil { + log.Error().Err(err).Msg("[processing] template execute error") + continue + } + message := tpl.String() + + // to chat + err = b.sendTo(b.telegramChats, message) + if err != nil { + log.Error().Err(err).Msg("[processing] send to chat") + continue + } + } + + return 0, nil +} diff --git a/template.go b/template.go index 39b2a36..775b3c4 100644 --- a/template.go +++ b/template.go @@ -21,15 +21,22 @@ var balanceTemplate = `{{ .Name }} {{end}}` // Report template, Use the ReportPage structure -var reportPageTemplate = `Витрачено: {{ normalizePrice .SpentTotal }}{{ getCurrencySymbol .CurrencyCode }}, Кешбек: {{ normalizePrice .CashbackAmountTotal }}{{ getCurrencySymbol .CurrencyCode }} +var reportPageTemplate = `{{ $symbol := getCurrencySymbol .CurrencyCode }}Витрачено: {{ normalizePrice .SpentTotal }}{{ $symbol }}, Кешбек: {{ normalizePrice .CashbackAmountTotal }}{{ $symbol }} -{{range $item := .StatementItems }}{{ getIcon $item }} {{ normalizePrice $item.Amount }}{{ getCurrencySymbol .CurrencyCode }} {{ if ne $item.Amount $item.OperationAmount }} ({{ normalizePrice $item.OperationAmount }}{{ getCurrencySymbol $item.CurrencyCode }}){{end}}{{if $item.CashbackAmount }}, Кешбек: {{ normalizePrice $item.CashbackAmount }}{{ getCurrencySymbol $item.CurrencyCode }}{{end}} +{{range $item := .StatementItems }}{{ getIcon $item }} {{ normalizePrice $item.Amount }}{{ $symbol }} {{ if ne $item.Amount $item.OperationAmount }} ({{ normalizePrice $item.OperationAmount }}{{ getCurrencySymbol $item.CurrencyCode }}){{end}}{{if $item.CashbackAmount }}, Кешбек: {{ normalizePrice $item.CashbackAmount }}{{ getCurrencySymbol $item.CurrencyCode }}{{end}} {{ unescapeString $item.Description }}{{if $item.Comment }} Коментар: {{ unescapeString $item.Comment }}{{end}} -Баланс: {{ normalizePrice $item.Balance }}{{ getCurrencySymbol $item.CurrencyCode }} +Баланс: {{ normalizePrice $item.Balance }}{{ $symbol }} {{end}}` +// Schedule Report template, Use the ScheduleReportData structure +var scheduleReportTemplate = `Щоденна статистика рахунків, {{ .ClientInfo.Name }} + +Витрачено: {{ normalizePrice .Sum }} UAH +Кешбек: {{ normalizePrice .CashbackSum }} UAH +Транзакцій: {{ .Count }}` + // WebHook template, use the ClientInfo structure var webhookTemplate = `Вебхук: {{if .WebHookURL }}{{ .WebHookURL }}{{else}} Відсутній {{end}}` diff --git a/tools.go b/tools.go index 771454d..451b8bf 100644 --- a/tools.go +++ b/tools.go @@ -1,12 +1,16 @@ package main import ( + "bytes" + "encoding/gob" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" + "os" + "path/filepath" "strconv" "strings" "time" @@ -127,12 +131,7 @@ func getTimeRangeByPeriod(period string) (int64, int64, error) { return from, to, errors.New("incorrect period") } - kiev, err := time.LoadLocation("Europe/Kiev") - if err != nil { - return from, to, err - } - - now := time.Now().In(kiev) + now := time.Now().UTC() year, month, day := now.Date() switch period { @@ -246,3 +245,40 @@ func DoRequest[D any](data D, req *http.Request) (D, error) { log.Debug().Msgf("[DoRequest] responce %s", string(body)) return data, nil } + +func dumpToFile[T any](filePath string, data T) error { + f, err := os.Create(filepath.Clean(filePath)) + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + + err = enc.Encode(data) + if err != nil { + return err + } + _, err = f.Write(buf.Bytes()) + return err +} + +func dumpFromFile[T any](filePath string) (*T, error) { + f, err := os.Open(filepath.Clean(filePath)) + if err != nil { + return nil, err + } + defer f.Close() + + var buf bytes.Buffer + _, err = buf.ReadFrom(f) + if err != nil { + return nil, err + } + dec := gob.NewDecoder(&buf) + + data := new(T) + err = dec.Decode(data) + return data, err +} diff --git a/tools_test.go b/tools_test.go index 17d596d..9a01568 100644 --- a/tools_test.go +++ b/tools_test.go @@ -1,6 +1,12 @@ package main -import "testing" +import ( + "testing" + "time" + + "github.com/agiledragon/gomonkey/v2" + "github.com/stretchr/testify/assert" +) func TestGetPaginateButtonsPage1(t *testing.T) { total := 57 @@ -274,3 +280,28 @@ func TestGetPaginateButtonsCouplePage2(t *testing.T) { } } } + +func TestGetTimeRangeByPeriod(t *testing.T) { + patches := gomonkey.ApplyFunc(time.Now, func() time.Time { + return time.Unix(1672531300, 0) + }) + defer patches.Reset() + + from, to, err := getTimeRangeByPeriod("Today") + assert.NoError(t, err) + + assert.Equal(t, from, int64(1672531200)) + assert.Equal(t, to, int64(0)) + + from, to, err = getTimeRangeByPeriod("This week") + assert.NoError(t, err) + + assert.Equal(t, from, int64(1703462400)) + assert.Equal(t, to, int64(0)) + + from, to, err = getTimeRangeByPeriod("December") + assert.NoError(t, err) + + assert.Equal(t, from, int64(1701388800)) + assert.Equal(t, to, int64(1704067200)) +}