add cron job to sync currencies once in 24 hours

This commit is contained in:
Fran Jurmanović
2023-04-13 18:10:21 +02:00
parent cf1d4f8b1a
commit 84b00a9ddf
22 changed files with 407 additions and 25 deletions

View File

@@ -1,12 +1,16 @@
package api
import (
"log"
"time"
"wallet-api/pkg/controller"
"wallet-api/pkg/job"
"wallet-api/pkg/middleware"
"wallet-api/pkg/utl/common"
"wallet-api/pkg/utl/configs"
"github.com/gin-gonic/gin"
"github.com/go-co-op/gocron"
"github.com/go-pg/pg/v10"
"go.uber.org/dig"
)
@@ -22,6 +26,10 @@ Initializes web api controllers and its corresponding routes.
*/
func Routes(s *gin.Engine, db *pg.DB) {
c := dig.New()
scheduler := gocron.NewScheduler(time.UTC)
scheduler.SetMaxConcurrentJobs(3, 1)
defer scheduler.StartAsync()
ver := s.Group(configs.Prefix)
routeGroups := &common.RouteGroups{
@@ -46,5 +54,13 @@ func Routes(s *gin.Engine, db *pg.DB) {
c.Provide(func() *pg.DB {
return db
})
c.Provide(func() *gocron.Scheduler {
return scheduler
})
c.Provide(func() *log.Logger {
return log.Default()
})
controller.InitializeControllers(c)
job.InitializeJobs(c)
}

View File

@@ -2,13 +2,14 @@ package controller
import (
"fmt"
"go.uber.org/dig"
"strconv"
"strings"
"wallet-api/pkg/model"
"wallet-api/pkg/service"
"wallet-api/pkg/utl/common"
"go.uber.org/dig"
"github.com/gin-gonic/gin"
)
@@ -21,17 +22,18 @@ Initializes Dependency Injection modules and registers controllers
*dig.Container: Dig Container
*/
func InitializeControllers(c *dig.Container) {
service.InitializeServices(c)
controllerContainer := c.Scope("controller")
service.InitializeServices(controllerContainer)
c.Invoke(NewApiController)
c.Invoke(NewUserController)
c.Invoke(NewWalletController)
c.Invoke(NewWalletHeaderController)
c.Invoke(NewTransactionController)
c.Invoke(NewTransactionStatusController)
c.Invoke(NewTransactionTypeController)
c.Invoke(NewSubscriptionController)
c.Invoke(NewSubscriptionTypeController)
controllerContainer.Invoke(NewApiController)
controllerContainer.Invoke(NewUserController)
controllerContainer.Invoke(NewWalletController)
controllerContainer.Invoke(NewWalletHeaderController)
controllerContainer.Invoke(NewTransactionController)
controllerContainer.Invoke(NewTransactionStatusController)
controllerContainer.Invoke(NewTransactionTypeController)
controllerContainer.Invoke(NewSubscriptionController)
controllerContainer.Invoke(NewSubscriptionTypeController)
}

52
pkg/job/currency.go Normal file
View File

@@ -0,0 +1,52 @@
package job
import (
"context"
"log"
"wallet-api/pkg/service"
"wallet-api/pkg/utl/common"
"github.com/go-co-op/gocron"
)
type CurrencyController struct {
service *service.CurrencyService
logger *log.Logger
scheduler *gocron.Scheduler
}
/*
NewCurrencyJob
Initializes CurrencyJob.
Args:
*services.CurrencyService: Currency service
*gin.RouterGroup: Gin Router Group
Returns:
*CurrencyJob: Job for "Currency" route interactions
*/
func NewCurrencyJob(as *service.CurrencyService, scheduler *gocron.Scheduler, logger *log.Logger) *CurrencyController {
currencyScheduler := scheduler.Tag("currency")
wc := &CurrencyController{
service: as,
logger: logger,
scheduler: currencyScheduler,
}
_, err := currencyScheduler.Every(1).Days().Do(wc.Sync)
common.CheckError(err)
currencyScheduler.StartAsync()
log.Println("CurrencyJob started")
return wc
}
func (wc *CurrencyController) Sync() {
wc.logger.Println("CurrencyJob: Syncing currencies")
ctx := context.Background()
wc.service.Sync(ctx)
wc.logger.Println("CurrencyJob: Syncing currencies done")
}

33
pkg/job/job.go Normal file
View File

@@ -0,0 +1,33 @@
package job
import (
"log"
"os"
"wallet-api/pkg/service"
"wallet-api/pkg/utl/common"
"go.uber.org/dig"
)
/*
InitializeJobs
Initializes Dependency Injection modules and registers Jobs
Args:
*dig.Container: Dig Container
*/
func InitializeJobs(c *dig.Container) {
file, err := os.OpenFile("job.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
common.CheckError(err)
logger := log.New(file, "Job: ", log.Ldate|log.Ltime|log.Lshortfile)
jobContainer := c.Scope("job")
jobContainer.Provide(func() *log.Logger {
return logger
})
service.InitializeServices(jobContainer)
jobContainer.Invoke(NewCurrencyJob)
}

View File

@@ -27,7 +27,7 @@ func CreateTableTransactionStatus(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -0,0 +1,40 @@
package migrate
import (
"fmt"
"log"
"wallet-api/pkg/model"
"github.com/go-pg/pg/v10"
"github.com/go-pg/pg/v10/orm"
)
/*
CreateTableCurrencies
Creates Currencies table if it does not exist.
Args:
*pg.DB: Postgres database client
Returns:
error: Returns if there is an error with table creation
*/
func CreateTableCurrencies(db *pg.Tx) error {
models := []interface{}{
(*model.Currency)(nil),
}
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: true,
FKConstraints: true,
})
if err != nil {
log.Printf("Error creating table \"currencies\": %s", err)
return err
} else {
fmt.Println("Table \"currencies\" created successfully")
}
}
return nil
}

View File

@@ -27,7 +27,7 @@ func CreateTableWallets(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -27,7 +27,7 @@ func CreateTableTransactionTypes(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -27,7 +27,7 @@ func CreateTableTransactions(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -27,7 +27,7 @@ func CreateTableSubscriptionTypes(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -26,7 +26,7 @@ func CreateTableSubscriptions(db *pg.Tx) error {
for _, model := range models {
err := db.Model(model).CreateTable(&orm.CreateTableOptions{
IfNotExists: false,
IfNotExists: true,
FKConstraints: true,
})
if err != nil {

View File

@@ -48,12 +48,19 @@ func Start(conn *pg.DB, version string) []error {
CreateTableTransactions,
},
}
migration005 := Migration{
Version: "005",
Migrations: []interface{}{
CreateTableCurrencies,
},
}
migrationsMap := []Migration{
migration001,
migration002,
migration003,
migration004,
migration005,
}
var errors []error

63
pkg/model/currency.go Normal file
View File

@@ -0,0 +1,63 @@
package model
import (
"encoding/json"
"math"
)
type Currency struct {
tableName struct{} `pg:"currencies,alias:currencies"`
BaseModel
Rate float32 `json:"rate", pg:"rate,default:0"`
Name string `json:"name", pg:"name,unique"`
}
type CurrencyEdit struct {
tableName struct{} `pg:"currencies,alias:currencies"`
Id string `json:"id" form:"id"`
Rate json.Number `json:"rate", form:"rate"`
Name string `json:"name", form:"name"`
}
type NewCurrencyBody struct {
Rate json.Number `json:"rate", form:"rate"`
Name string `json:"name", form:"name"`
}
type ExchangeBody struct {
Base string `json:"base"`
Rates interface{} `json:"rates"`
}
type Rate struct {
Code string `json:"code"`
Rate float64 `json:"rate"`
}
func (body *CurrencyEdit) ToCurrency() *Currency {
rate, _ := body.Rate.Float64()
tm := new(Currency)
tm.Id = body.Id
tm.Rate = float32(math.Round(rate*100) / 100)
tm.Name = body.Name
return tm
}
func (body *NewCurrencyBody) ToCurrency() *Currency {
rate, _ := body.Rate.Float64()
tm := new(Currency)
tm.Init()
tm.Rate = float32(math.Round(rate*100) / 100)
tm.Name = body.Name
return tm
}
func (body *ExchangeBody) Unmarshal(resp *[]interface{}) *ExchangeBody {
body.Base = (*resp)[3].(string)
// body.Rates = []Rate{}
// for k, v := range (*resp)[5].(map[string]interface{}) {
// body.Rates = append(body.Rates, Rate{Code: k, Rate: v.(float64)})
// }
return body
}

View File

@@ -0,0 +1,84 @@
package repository
import (
"context"
"log"
"time"
"wallet-api/pkg/model"
"github.com/go-pg/pg/v10"
)
type CurrencyRepository struct {
db *pg.DB
logger *log.Logger
}
func NewCurrencyRepository(db *pg.DB, logger *log.Logger) *CurrencyRepository {
return &CurrencyRepository{
db: db,
logger: logger,
}
}
/*
GetFirst
Gets first row from Currency table.
Args:
context.Context: Application context
Returns:
model.CurrencyModel: Currency object from database.
*/
func (as CurrencyRepository) Sync(ctx context.Context, rate *model.Rate, tx *pg.Tx) *model.Currency {
currency := new(model.Currency)
currency.Name = rate.Code
any := tx.Model(currency).Where("name = ?", rate.Code).First()
currency.Rate = float32(rate.Rate)
if any != nil {
currency.Init()
_, err := tx.Model(currency).Insert()
if err != nil {
as.logger.Println(err)
return nil
}
} else {
currency.DateUpdated = time.Now()
_, err := tx.Model(currency).WherePK().Update()
if err != nil {
as.logger.Println(err)
return nil
}
}
return currency
}
/*
GetFirst
Gets first row from Currency table.
Args:
context.Context: Application context
Returns:
model.CurrencyModel: Currency object from database.
*/
func (as CurrencyRepository) SyncBulk(ctx context.Context, rates *[]model.Rate) *[]model.Currency {
tx, _ := as.db.BeginContext(ctx)
defer tx.Rollback()
currencies := new([]model.Currency)
for _, r := range *rates {
currency := as.Sync(ctx, &r, tx)
if currency != nil {
*currencies = append(*currencies, *currency)
}
}
tx.Commit()
return currencies
}

View File

@@ -1,10 +1,11 @@
package repository
import (
"go.uber.org/dig"
"wallet-api/pkg/model"
"wallet-api/pkg/utl/common"
"go.uber.org/dig"
"github.com/go-pg/pg/v10"
)
@@ -16,7 +17,7 @@ Initializes Dependency Injection modules for repositories
Args:
*dig.Container: Dig Container
*/
func InitializeRepositories(c *dig.Container) {
func InitializeRepositories(c *dig.Scope) {
c.Provide(NewApiRepository)
c.Provide(NewSubscriptionRepository)
c.Provide(NewSubscriptionTypeRepository)
@@ -25,6 +26,7 @@ func InitializeRepositories(c *dig.Container) {
c.Provide(NewTransactionTypeRepository)
c.Provide(NewUserRepository)
c.Provide(NewWalletRepository)
c.Provide(NewCurrencyRepository)
}
/*

50
pkg/service/currency.go Normal file
View File

@@ -0,0 +1,50 @@
package service
import (
"context"
"log"
"wallet-api/pkg/model"
"wallet-api/pkg/repository"
"wallet-api/pkg/utl/common"
)
type CurrencyService struct {
repository *repository.CurrencyRepository
logger *log.Logger
}
func NewCurrencyService(repository *repository.CurrencyRepository, logger *log.Logger) *CurrencyService {
return &CurrencyService{
repository: repository,
logger: logger,
}
}
/*
GetFirst
Gets first row from Currency table.
Args:
context.Context: Application context
Returns:
model.CurrencyModel: Currency object from database.
*/
func (as CurrencyService) Sync(ctx context.Context) {
resp, err := common.Fetch[model.ExchangeBody]("GET", "https://api.exchangerate-api.com/v4/latest/euro")
if err != nil {
as.logger.Println(err)
return
}
m := resp.Rates.(map[string]interface{})
rates := new([]model.Rate)
for k, v := range m {
rate := new(model.Rate)
rate.Code = k
rate.Rate = v.(float64)
*rates = append(*rates, *rate)
}
as.repository.SyncBulk(ctx, rates)
}

View File

@@ -14,7 +14,7 @@ Initializes Dependency Injection modules for services
Args:
*dig.Container: Dig Container
*/
func InitializeServices(c *dig.Container) {
func InitializeServices(c *dig.Scope) {
repository.InitializeRepositories(c)
c.Provide(NewApiService)
@@ -25,4 +25,5 @@ func InitializeServices(c *dig.Container) {
c.Provide(NewTransactionTypeService)
c.Provide(NewUserService)
c.Provide(NewWalletService)
c.Provide(NewCurrencyService)
}

View File

@@ -1,12 +1,16 @@
package common
import (
"github.com/gin-gonic/gin"
"encoding/json"
"io"
"log"
"net"
"net/http"
"os"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
type RouteGroups struct {
@@ -61,3 +65,24 @@ func Find[T any](lst *[]T, callback func(item *T) bool) *T {
}
return nil
}
func Fetch[T any](method string, url string) (*T, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
data := new(T)
err = json.Unmarshal(body, data)
if err != nil {
return nil, err
}
return data, nil
}