From 634cbce8aaea56b587ffc642fe9afbf190bd2e05 Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 22 May 2023 19:48:44 +0000 Subject: [PATCH] feat: translate currency --- app/app.go | 4 +- dal/quote.go | 7 ++- go.mod | 23 ++++++- go.sum | 3 + handlers/quote.go | 48 ++++++++++++++- openapi.yaml | 11 +++- utils/array_to_map.go | 11 ++++ utils/array_to_map_test.go | 69 +++++++++++++++++++++ utils/currency_exchange.go | 38 ++++++++++++ utils/currency_exchange_test.go | 106 ++++++++++++++++++++++++++++++++ 10 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 utils/array_to_map.go create mode 100644 utils/array_to_map_test.go create mode 100644 utils/currency_exchange.go create mode 100644 utils/currency_exchange_test.go diff --git a/app/app.go b/app/app.go index f2f54d1..d9cf13d 100644 --- a/app/app.go +++ b/app/app.go @@ -3,6 +3,8 @@ package app import ( "context" "errors" + "net/http" + "github.com/danilsolovyov/croupierCbrf/dal" "github.com/danilsolovyov/croupierCbrf/handlers" "github.com/danilsolovyov/croupierCbrf/worker" @@ -10,7 +12,6 @@ import ( "github.com/skeris/appInit" "github.com/themakers/hlog" "go.uber.org/zap" - "net/http" //"net/http" "os" @@ -156,6 +157,7 @@ func New(ctx context.Context, options interface{}) (appInit.CommonApp, error) { handler := handlers.NewHandler(connMongo, hlogger, "") router.HandleFunc("/getQuotes", handler.GetQuotes) + router.HandleFunc("/change", handler.Translate) // Startup server srv := &http.Server{Addr: opts.AppAddr, Handler: router} diff --git a/dal/quote.go b/dal/quote.go index 8889a2c..a925905 100644 --- a/dal/quote.go +++ b/dal/quote.go @@ -4,10 +4,11 @@ import ( "context" "errors" "fmt" + "time" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "time" ) type Quote struct { @@ -21,6 +22,10 @@ type Quote struct { UpdatedAt time.Time `bson:"UpdatedAt" json:"updated-at"` } +const ( + DefaultCurrency = "RUB" +) + // UpdateQuote with upsert option func (mc *MongoConnection) UpdateQuote(ctx context.Context, record *Quote) error { filter := bson.M{"_id": record.ID} diff --git a/go.mod b/go.mod index debcaaa..134ef54 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,31 @@ module github.com/danilsolovyov/croupierCbrf -go 1.16 +go 1.18 require ( - github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/mux v1.8.0 github.com/skeris/appInit v0.1.12 + github.com/stretchr/testify v1.7.0 github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf go.mongodb.org/mongo-driver v1.8.3 go.uber.org/zap v1.21.0 golang.org/x/text v0.3.7 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.0.2 // indirect + github.com/xdg-go/stringprep v1.0.2 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum index 7ebb7e6..02115c5 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -104,6 +106,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/handlers/quote.go b/handlers/quote.go index 5bbe4f9..fa2910a 100644 --- a/handlers/quote.go +++ b/handlers/quote.go @@ -2,11 +2,14 @@ package handlers import ( "net/http" + "strconv" + + "github.com/danilsolovyov/croupierCbrf/dal" + "github.com/danilsolovyov/croupierCbrf/utils" ) func (h *Handler) GetQuotes(w http.ResponseWriter, r *http.Request) { result, err := h.mongo.GetQuoteList(r.Context()) - if err != nil { h.reportError(w, r, http.StatusInternalServerError, err.Error(), err) return @@ -17,3 +20,46 @@ func (h *Handler) GetQuotes(w http.ResponseWriter, r *http.Request) { Message: result, }) } + +func (h *Handler) Translate(response http.ResponseWriter, request *http.Request) { + params := request.URL.Query() + + currencyFrom := params.Get("currencyFrom") + currencyTo := params.Get("currencyTo") + value := params.Get("value") + + if currencyFrom == "" { + currencyFrom = dal.DefaultCurrency + } + + if currencyTo == "" { + currencyTo = dal.DefaultCurrency + } + + parsedValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + h.reportError(response, request, http.StatusBadRequest, "invalid value format", err) + + return + } + + quotes, err := h.mongo.GetQuoteList(request.Context()) + if err != nil { + h.reportError(response, request, http.StatusInternalServerError, err.Error(), err) + return + } + + quotesMap := utils.ArrayToMap(quotes, func(quote dal.Quote) string { + return quote.ID + }) + + result, err := utils.CurrencyExchange(quotesMap, currencyFrom, currencyTo, parsedValue) + if err != nil { + h.reportError(response, request, http.StatusInternalServerError, err.Error(), err) + } + + h.sendResponse(response, request, Response{ + Success: true, + Message: result, + }) +} diff --git a/openapi.yaml b/openapi.yaml index 1910641..75c3f6d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -14,11 +14,15 @@ paths: - currencies description: Получить количество денег в пересчёте на десятые части указанной валюты. На вход подаются копейки. parameters: - - name: cur + - name: currencyFrom in: query - description: валюта, в которую надо перевести переданнео число + description: валюта, из которой надо перевести переданное число required: true - - name: val + - name: currencyTo + in: query + description: валюта, в которую надо перевести переданное число + required: true + - name: value in: query description: количество копеек, которые надо перевести в указанную валюту required: true @@ -32,4 +36,5 @@ paths: properties: money: type: integer + format: int64 example: 12065 diff --git a/utils/array_to_map.go b/utils/array_to_map.go new file mode 100644 index 0000000..3b36e7b --- /dev/null +++ b/utils/array_to_map.go @@ -0,0 +1,11 @@ +package utils + +func ArrayToMap[T any, V comparable](array []T, key func(T) V) map[V]T { + result := make(map[V]T, len(array)) + + for _, element := range array { + result[key(element)] = element + } + + return result +} diff --git a/utils/array_to_map_test.go b/utils/array_to_map_test.go new file mode 100644 index 0000000..9ebf735 --- /dev/null +++ b/utils/array_to_map_test.go @@ -0,0 +1,69 @@ +package utils_test + +import ( + "testing" + + "github.com/danilsolovyov/croupierCbrf/utils" + "github.com/stretchr/testify/assert" +) + +func TestArrayToMap(t *testing.T) { + type testModel struct { + ID string + } + + t.Run("Успешная конвертация массива в map без коллизий", func(t *testing.T) { + assert.NotPanics(t, func() { + result := utils.ArrayToMap( + []testModel{{ID: "test"}, {ID: "test2"}}, + func(model testModel) string { + return model.ID + }, + ) + + assert.Equal(t, + map[string]testModel{ + "test": {ID: "test"}, + "test2": {ID: "test2"}, + }, + result, + ) + }) + }) + + t.Run("Успешная конвертация массива в map с коллизиями", func(t *testing.T) { + assert.NotPanics(t, func() { + result := utils.ArrayToMap( + []testModel{{ID: "test"}, {ID: "test"}}, + func(model testModel) string { + return model.ID + }, + ) + + assert.Equal(t, + map[string]testModel{ + "test": {ID: "test"}, + }, + result, + ) + }) + }) + + t.Run("Успешная конвертация массива в map с функцией, возаращающая пустую строку", func(t *testing.T) { + assert.NotPanics(t, func() { + result := utils.ArrayToMap( + []testModel{{ID: "test"}, {ID: "test"}}, + func(model testModel) string { + return "" + }, + ) + + assert.Equal(t, + map[string]testModel{ + "": {ID: "test"}, + }, + result, + ) + }) + }) +} diff --git a/utils/currency_exchange.go b/utils/currency_exchange.go new file mode 100644 index 0000000..535a26b --- /dev/null +++ b/utils/currency_exchange.go @@ -0,0 +1,38 @@ +package utils + +import ( + "fmt" + "math" + + "github.com/danilsolovyov/croupierCbrf/dal" +) + +func CurrencyExchange(currencies map[string]dal.Quote, fromCurrency, toCurrency string, amount int64) (int64, error) { + fromRate, ok := currencies[fromCurrency] + if !ok && fromCurrency != dal.DefaultCurrency { + return 0, fmt.Errorf("Курс для валюты %s не найден", fromCurrency) + } + if fromCurrency == dal.DefaultCurrency { + fromRate.Value = 1 + } + + toRate, ok := currencies[toCurrency] + if !ok && toCurrency != dal.DefaultCurrency { + return 0, fmt.Errorf("Курс для валюты %s не найден", toCurrency) + } + if toCurrency == dal.DefaultCurrency { + toRate.Value = 1 + } + + // Округляем число курса 79.9093 => 79.91 + roundedFromValue := math.Round(fromRate.Value*100) / 100 + roundedToValue := math.Round(toRate.Value*100) / 100 + + // Переводим сумму из одной валюты в рубли + amountInRubles := float64(amount) * roundedFromValue + + // Переводим рубли в нужную валюту и округляем до ближайшего целого числа + result := int64(amountInRubles / roundedToValue) + + return result, nil +} diff --git a/utils/currency_exchange_test.go b/utils/currency_exchange_test.go new file mode 100644 index 0000000..11213b2 --- /dev/null +++ b/utils/currency_exchange_test.go @@ -0,0 +1,106 @@ +package utils_test + +import ( + "fmt" + "testing" + + "github.com/danilsolovyov/croupierCbrf/dal" + "github.com/danilsolovyov/croupierCbrf/utils" + "github.com/stretchr/testify/assert" +) + +func TestCurrencyExchange(t *testing.T) { + currencies := map[string]dal.Quote{ + "USD": {ID: "USD", Value: 79.9093}, + "EUR": {ID: "EUR", Value: 86.2770}, + "BYN": {ID: "BYN", Value: 27.3110}, + } + + testCases := []struct { + from string + to string + value int64 + expected int64 + isError bool + }{ + { + from: "USD", + to: "RUB", + value: 10000, + expected: 799100, + }, + { + from: "RUB", + to: "RUB", + value: 10000, + expected: 10000, + }, + { + from: "BYN", + to: "RUB", + value: 10000, + expected: 273100, + }, + { + from: "EUR", + to: "RUB", + value: 10000, + expected: 862800, + }, + { + from: "RUB", + to: "USD", + value: 799100, + expected: 10000, + }, + { + from: "RUB", + to: "BYN", + value: 273100, + expected: 10000, + }, + { + from: "RUB", + to: "EUR", + value: 862800, + expected: 10000, + }, + { + from: "BYN", + to: "EUR", + value: 10000, + expected: 3165, + }, + { + from: "BYNNN", + to: "EURRR", + value: 19900, + expected: 0, + isError: true, + }, + { + from: "BYN", + to: "EURRR", + value: 19900, + expected: 0, + isError: true, + }, + } + + for _, testCase := range testCases { + result, err := utils.CurrencyExchange(currencies, testCase.from, testCase.to, testCase.value) + + if testCase.isError { + assert.Equal(t, testCase.expected, result) + assert.Error(t, err) + continue + } + + assert.NoError(t, err) + assert.Equal(t, + testCase.expected, + result, + fmt.Sprintf("Неверный результат при переводе с %s на %s", testCase.from, testCase.to), + ) + } +}