From 5b11756af6702e364ecba24c7d4a2ebf3794dc7a Mon Sep 17 00:00:00 2001
From: netscrawler <mariarthyjamesoo@gmail.com>
Date: Thu, 4 Jul 2024 00:44:15 +0300
Subject: [PATCH] v0.1

---
 .idea/changeAPI.iml                      |   1 -
 .idea/modules.xml                        |   1 -
 .idea/vcs.xml                            |   1 -
 Taskfile.yaml                            |   9 ++
 cmd/converter/main.go                    |  36 ++++-
 config/local.yaml                        |   6 +-
 go.mod                                   |  13 +-
 go.sum                                   |  19 ++-
 internal/app/app.go                      |   1 +
 internal/app/grpc/app.go                 |   5 +-
 internal/config/config.go                |   7 +
 internal/domain/models/vunitRate.go      |   6 +
 internal/grpc/cnvrt/server.go            |  47 +++++-
 internal/lib/rateExtract/rateExtract.go  | 177 +++++++++++++++++++++++
 internal/services/converter/converter.go |  48 ++++++
 15 files changed, 354 insertions(+), 23 deletions(-)
 create mode 100644 Taskfile.yaml
 create mode 100644 internal/domain/models/vunitRate.go
 create mode 100644 internal/lib/rateExtract/rateExtract.go
 create mode 100644 internal/services/converter/converter.go

diff --git a/.idea/changeAPI.iml b/.idea/changeAPI.iml
index 728392f..5e764c4 100644
--- a/.idea/changeAPI.iml
+++ b/.idea/changeAPI.iml
@@ -5,6 +5,5 @@
     <content url="file://$MODULE_DIR$" />
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="module" module-name="protos" />
   </component>
 </module>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index fdcfa42..33a3860 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -3,7 +3,6 @@
   <component name="ProjectModuleManager">
     <modules>
       <module fileurl="file://$PROJECT_DIR$/.idea/changeAPI.iml" filepath="$PROJECT_DIR$/.idea/changeAPI.iml" />
-      <module fileurl="file://$PROJECT_DIR$/../protos/.idea/protos.iml" filepath="$PROJECT_DIR$/../protos/.idea/protos.iml" />
     </modules>
   </component>
 </project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 8e4e17d..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,6 +2,5 @@
 <project version="4">
   <component name="VcsDirectoryMappings">
     <mapping directory="" vcs="Git" />
-    <mapping directory="$PROJECT_DIR$/../protos" vcs="Git" />
   </component>
 </project>
\ No newline at end of file
diff --git a/Taskfile.yaml b/Taskfile.yaml
new file mode 100644
index 0000000..466ad7f
--- /dev/null
+++ b/Taskfile.yaml
@@ -0,0 +1,9 @@
+version: "3"
+
+tasks:
+  runServer:
+    aliases:
+      - run
+    desc: "Run grpc service"
+    cmds:
+     - go run cmd/converter/main.go --config .\config\local.yaml
\ No newline at end of file
diff --git a/cmd/converter/main.go b/cmd/converter/main.go
index b0ed0d3..04024c0 100644
--- a/cmd/converter/main.go
+++ b/cmd/converter/main.go
@@ -1,12 +1,16 @@
 package main
 
 import (
+	"context"
 	"converter/internal/app"
 	"converter/internal/config"
 	"converter/internal/lib/logger/handlers/slogpretty"
 	"fmt"
+	"github.com/redis/go-redis/v9"
 	"log/slog"
 	"os"
+	"os/signal"
+	"syscall"
 )
 
 const (
@@ -24,10 +28,40 @@ func main() {
 	log := setupLogger(cfg.Env)
 	log.Info("starting application", slog.Any("cfg", cfg))
 	applicaton := app.New(log, cfg.GRPC.Port, cfg.TokenTTL)
-	applicaton.GRPCSrv.MustRun()
+	go applicaton.GRPCSrv.MustRun()
+
 	// TODO: инициализировать приложение app
 
+	//TODO:redis
+	client := redis.NewClient(&redis.Options{
+		Addr:     cfg.Redis.Addr,
+		Password: cfg.Redis.Password,
+		DB:       cfg.Redis.DB,
+	})
+
+	ctx := context.Background()
+
+	err := client.Set(ctx, "key", "value1", 0).Err()
+	if err != nil {
+		log.Info("err")
+	}
+
+	val, err := client.Get(ctx, "key").Result()
+	if err != nil {
+		log.Info("err")
+	}
+	log.Info("key", slog.Any("val", val))
+
 	// TODO: запустить gRPC сервер приложения
+
+	// Graceful shutdown
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
+
+	sign := <-stop
+	log.Info("stoping application", slog.String("signal", sign.String()))
+	applicaton.GRPCSrv.Stop()
+	log.Info("application stopped")
 }
 
 func setupLogger(env string) *slog.Logger {
diff --git a/config/local.yaml b/config/local.yaml
index 1db89b1..374baaf 100644
--- a/config/local.yaml
+++ b/config/local.yaml
@@ -2,4 +2,8 @@ env: "local"
 token_ttl: 1h
 grpc:
   port: 44044
-  timeout: 10h
\ No newline at end of file
+  timeout: 10h
+redis:
+  addr: "localhost:6379"
+  password: "1234"
+  db: 0
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 2bce6cc..e56c53c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,23 +3,26 @@ module converter
 go 1.22
 
 require (
-	github.com/netscrawler/protos v0.0.5
+	github.com/fatih/color v1.17.0
+	github.com/ilyakaznacheev/cleanenv v1.5.0
+	github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4
+	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
 	google.golang.org/grpc v1.64.0
 )
 
 require (
 	github.com/BurntSushi/toml v1.2.1 // indirect
-	github.com/fatih/color v1.17.0 // indirect
-	github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/joho/godotenv v1.5.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
+	github.com/redis/go-redis/v9 v9.5.3 // indirect
 	golang.org/x/net v0.22.0 // indirect
 	golang.org/x/sys v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
+	google.golang.org/protobuf v1.34.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
 )
diff --git a/go.sum b/go.sum
index aeb3402..3c7551d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,13 @@
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
 github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
 github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -11,10 +17,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/netscrawler/protos v0.0.0-20240629165054-1454eca14db0 h1:YKByh7dtXFBif3xemj/OFyWbAcw3j/M8tD4U4XF229A=
-github.com/netscrawler/protos v0.0.0-20240629165054-1454eca14db0/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc=
-github.com/netscrawler/protos v0.0.4 h1:D+Cb4cELT9YSIyUsyXL233Zb90ClzOR290aE4i+KI8Y=
-github.com/netscrawler/protos v0.0.4/go.mod h1:jgfFUikzJpivreejTHY7s1BWvVpM9/rJdvTntTzcPzc=
+github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4 h1:V/zWems1vjc1Ob0p0APeL+tKiSHeOW4Kc1yo3xP0IMQ=
+github.com/netscrawler/protoss v0.0.0-20240630182512-36e5b935e6b4/go.mod h1:+Ms2tgnXO0rzzVXLn61kltLnEOA/CuSKSv62L+cacdQ=
+github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
+github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
 golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
@@ -29,8 +35,9 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
 google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
 google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 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/internal/app/app.go b/internal/app/app.go
index a4cb23b..b4c6a74 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -16,6 +16,7 @@ func New(
 	tokenTTl time.Duration) *App {
 	//TODO: инициализировать хранилище
 	//TODO: init convert service
+
 	grpcApp := grpcapp.New(log, grpcPort)
 	return &App{
 		GRPCSrv: grpcApp,
diff --git a/internal/app/grpc/app.go b/internal/app/grpc/app.go
index bdf25d2..6606e5a 100644
--- a/internal/app/grpc/app.go
+++ b/internal/app/grpc/app.go
@@ -2,6 +2,7 @@ package grpcapp
 
 import (
 	convertgrpc "converter/internal/grpc/cnvrt"
+	"converter/internal/services/converter"
 	"fmt"
 	"google.golang.org/grpc"
 	"log/slog"
@@ -18,8 +19,8 @@ func New(
 	log *slog.Logger,
 	port int) *App {
 	gRPCServer := grpc.NewServer()
-
-	convertgrpc.Register(gRPCServer)
+	convert := converter.New(log)
+	convertgrpc.Register(gRPCServer, convert)
 
 	return &App{
 		log:        log,
diff --git a/internal/config/config.go b/internal/config/config.go
index 565af1b..b801a0a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -11,6 +11,7 @@ type Config struct {
 	Env      string        `yaml:"env" env-default:"local"`
 	TokenTTL time.Duration `yaml:"token_ttl" env-required:"true"`
 	GRPC     GRPCConfig    `yaml:"grpc"`
+	Redis    RedisConfig   `yaml:"redis"`
 }
 
 type GRPCConfig struct {
@@ -18,6 +19,12 @@ type GRPCConfig struct {
 	Timeout time.Duration `yaml:"timeout"`
 }
 
+type RedisConfig struct {
+	Addr     string `yaml:"addr"`
+	Password string `yaml:"password"`
+	DB       int    `yaml:"db"`
+}
+
 func MustLoad() *Config {
 	path := fetchConfigPath()
 	if path == "" {
diff --git a/internal/domain/models/vunitRate.go b/internal/domain/models/vunitRate.go
new file mode 100644
index 0000000..d1d9a3d
--- /dev/null
+++ b/internal/domain/models/vunitRate.go
@@ -0,0 +1,6 @@
+package models
+
+type VunitRate struct {
+	Currency string
+	Rate     float32
+}
diff --git a/internal/grpc/cnvrt/server.go b/internal/grpc/cnvrt/server.go
index ab62b44..b5d94dd 100644
--- a/internal/grpc/cnvrt/server.go
+++ b/internal/grpc/cnvrt/server.go
@@ -2,21 +2,58 @@ package cnvrt
 
 import (
 	"context"
-	cnvrtv1 "github.com/netscrawler/protos/gen/go/changeAPI"
+	cnvrtv1 "github.com/netscrawler/protoss/gen/go/changeAPI"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 )
 
+type Converter interface {
+	Convert(ctx context.Context,
+		amount uint32,
+		targetCurrency string,
+	) (convertedAmount uint32, rate float32, err error)
+}
+
 type serverAPI struct {
 	cnvrtv1.UnimplementedConverterServer
+	convert Converter
 }
 
-func Register(gRPC *grpc.Server) {
-	cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{})
+func Register(gRPC *grpc.Server, convert Converter) {
+	cnvrtv1.RegisterConverterServer(gRPC, &serverAPI{convert: convert})
 }
 
-func (s serverAPI) Convert(
+func (s *serverAPI) Convert(
 	ctx context.Context,
 	req *cnvrtv1.ConvertRequest) (
 	*cnvrtv1.ConvertResponse, error) {
-	panic("implement me")
+	if !isValidCurrency(req.GetTargetCurrency()) {
+		return nil, status.Error(codes.InvalidArgument, "Invalid target currency")
+	}
+
+	convertedAmount, rate, err := s.convert.Convert(ctx, req.GetAmount(), req.GetTargetCurrency())
+	if err != nil {
+		//todo error handler
+		return nil, status.Error(codes.Internal, "Internal error")
+	}
+	return &cnvrtv1.ConvertResponse{
+		BaseAmount:        req.GetAmount(),
+		ConvertedAmount:   convertedAmount,
+		ConvertedCurrency: req.GetTargetCurrency(),
+		Rate:              rate,
+	}, nil
+}
+
+func isValidCurrency(currency string) bool {
+
+	currencies := map[string]bool{
+		"AUD": true, "GBP": true, "BYR": true, "DKK": true, "USD": true, "EUR": true,
+		"ISK": true, "KZT": true, "CAD": true, "NOK": true, "XDR": true, "SGD": true,
+		"TRL": true, "UAH": true, "SEK": true, "CHF": true, "JPY": true,
+	}
+	if currencies[currency] {
+		return true
+	}
+	return false
 }
diff --git a/internal/lib/rateExtract/rateExtract.go b/internal/lib/rateExtract/rateExtract.go
new file mode 100644
index 0000000..314ddc2
--- /dev/null
+++ b/internal/lib/rateExtract/rateExtract.go
@@ -0,0 +1,177 @@
+package rateExtract
+
+import (
+	"compress/gzip"
+	"converter/internal/domain/models"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"golang.org/x/text/encoding/charmap"
+)
+
+type resultType struct {
+	XMLName xml.Name `xml:"ValCurs"`
+	Valute  []struct {
+		NumCode  string `xml:"NumCode"`
+		CharCode string `xml:"CharCode"`
+		Nominal  string `xml:"Nominal"`
+		Name     string `xml:"Name"`
+		Value    string `xml:"Value"`
+	} `xml:"Valute"`
+}
+
+type cacheKeyType struct {
+	CurrencyId string
+	Date       string
+}
+
+type cacheResultType struct {
+	Rate float64
+}
+
+var urlTemplate string = "https://www.cbr.ru/scripts/XML_daily.asp?date_req=%s"
+
+var cache map[cacheKeyType]*cacheResultType
+
+func GetExchangeRate(currencyId string) (models.VunitRate, error) {
+	date := time.Now()
+	if cache == nil {
+		cache = map[cacheKeyType]*cacheResultType{}
+	}
+
+	reqDate := fmt.Sprintf("%02d/%02d/%d", date.Day(), date.Month(), date.Year())
+
+	cacheKey := cacheKeyType{CurrencyId: currencyId, Date: reqDate}
+
+	cacheResult, exists := cache[cacheKey]
+
+	if !exists {
+
+		url := fmt.Sprintf(urlTemplate, reqDate)
+
+		req, err := http.NewRequest(http.MethodGet, url, nil)
+		if err != nil {
+			return models.VunitRate{}, err
+		}
+
+		req.Header.Add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
+		req.Header.Add("accept-encoding", "gzip, deflate, br")
+		req.Header.Add("accept-language", "en-US,en;q=0.9,ru;q=0.8")
+		req.Header.Add("cache-control", "max-age=0")
+		req.Header.Add("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36")
+
+		client := &http.Client{}
+
+		resp, err := client.Do(req)
+		if err != nil {
+			return models.VunitRate{Currency: currencyId, Rate: 0}, err
+		}
+
+		defer resp.Body.Close()
+
+		var reader io.ReadCloser
+
+		switch resp.Header.Get("Content-Encoding") {
+		case "gzip":
+			reader, err = gzip.NewReader(resp.Body)
+			if err != nil {
+				return models.VunitRate{Currency: currencyId, Rate: 0}, err
+			}
+			defer reader.Close()
+		default:
+			reader = resp.Body
+		}
+
+		xml := xml.NewDecoder(reader)
+
+		xml.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
+			switch charset {
+			case "windows-1251":
+				return charmap.Windows1251.NewDecoder().Reader(input), nil
+			default:
+				return nil, fmt.Errorf("unknown charset: %s", charset)
+			}
+		}
+
+		result := &resultType{}
+
+		err = xml.Decode(result)
+		if err != nil {
+			return models.VunitRate{Currency: currencyId, Rate: 0}, err
+		}
+
+		for _, resultRow := range result.Valute {
+
+			if resultRow.CharCode != currencyId {
+				continue
+			}
+
+			resultRow.Value = strings.Replace(resultRow.Value, ",", ".", 1)
+
+			rate, err := strconv.ParseFloat(resultRow.Value, 64)
+			if err != nil {
+				return models.VunitRate{Currency: currencyId, Rate: 0}, err
+			}
+
+			nominal, err := strconv.ParseInt(resultRow.Nominal, 10, 64)
+			if err != nil {
+				return models.VunitRate{Currency: currencyId, Rate: 0}, err
+			}
+
+			cacheResult = &cacheResultType{Rate: rate / float64(nominal)}
+
+			cache[cacheKey] = cacheResult
+
+		}
+
+	}
+	return models.VunitRate{Currency: currencyId, Rate: float32(cacheResult.Rate)}, nil
+
+}
+
+//func Convert(from string, to string, value float64, date time.Time) (float64, error) {
+//
+//	if from == to {
+//		return value, nil
+//	}
+//
+//	if value == 0 {
+//		return 0, nil
+//	}
+//
+//	result := value
+//
+//	if from != CurrencyRUB {
+//
+//		exchangeRate, err := GetExchangeRate(from, date)
+//		if err != nil {
+//			return 0, err
+//		}
+//
+//		result = result * exchangeRate
+//
+//	}
+//
+//	if to != CurrencyRUB {
+//
+//		exchangeRate, err := GetExchangeRate(to, date)
+//		if err != nil {
+//			return 0, err
+//		}
+//
+//		result = result / exchangeRate
+//
+//	}
+//
+//	return (math.Floor(result*100) / 100), nil
+//
+//}
+//
+//func main() {
+//	fmt.Println(GetExchangeRate(CurrencyUSD, time.Now()))
+//}
diff --git a/internal/services/converter/converter.go b/internal/services/converter/converter.go
new file mode 100644
index 0000000..305f05d
--- /dev/null
+++ b/internal/services/converter/converter.go
@@ -0,0 +1,48 @@
+package converter
+
+import (
+	"context"
+	"converter/internal/lib/logger/sl"
+	"converter/internal/lib/rateExtract"
+	"fmt"
+	"log/slog"
+)
+
+type Converter struct {
+	log *slog.Logger
+}
+
+// New returns a new instance of the Converter service.
+func New(
+	log *slog.Logger,
+
+) *Converter {
+	return &Converter{
+		log: log,
+	}
+}
+
+func (c *Converter) Convert(
+	ctx context.Context,
+	amount uint32,
+	currency string,
+) (uint32, float32, error) {
+	const op = "converter.convert"
+	log := c.log.With(
+		slog.String("op", op),
+		slog.String("currency", currency),
+		slog.Any("amount", amount),
+	)
+	log.Info("convertation")
+	var convertedAmount uint32
+	//TODO extract rate
+	rate, err := rateExtract.GetExchangeRate(currency)
+	if err != nil {
+		log.Error("Error extracting rate", sl.Err(err))
+		return 0, 0, fmt.Errorf("%s: %w", op, err)
+	}
+
+	convertedAmount = uint32(float32(amount) * rate.Rate)
+
+	return convertedAmount, rate.Rate, nil
+}