Skip to content

Commit 4268d02

Browse files
authored
feat: sync exchange rates (#5)
* feat: currency exchange tests * feat: sync exchange rates * fix: delete currency
1 parent a2597b2 commit 4268d02

File tree

16 files changed

+405
-39
lines changed

16 files changed

+405
-39
lines changed

cmd/server/accounts.go

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ type AccountsApi struct {
1212
accSvc AccountSvc
1313
}
1414

15+
func (a *AccountsApi) ReorderAccounts(ctx context.Context, c *connect.Request[accountsv1.ReorderAccountsRequest]) (*connect.Response[accountsv1.ReorderAccountsResponse], error) {
16+
//TODO implement me
17+
panic("implement me")
18+
}
19+
1520
func (a *AccountsApi) DeleteAccount(ctx context.Context, c *connect.Request[accountsv1.DeleteAccountRequest]) (*connect.Response[accountsv1.DeleteAccountResponse], error) {
1621
// todo auth
1722

cmd/server/currency.go

+11
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ func (a *CurrencyApi) GetCurrencies(
6565
return connect.NewResponse(resp), nil
6666
}
6767

68+
func (a *CurrencyApi) DeleteCurrency(ctx context.Context, c *connect.Request[currencyv1.DeleteCurrencyRequest]) (*connect.Response[currencyv1.DeleteCurrencyResponse], error) {
69+
// todo auth
70+
71+
resp, err := a.currencySvc.DeleteCurrency(ctx, c.Msg)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
return connect.NewResponse(resp), nil
77+
}
78+
6879
func (a *CurrencyApi) CreateCurrency(
6980
ctx context.Context,
7081
c *connect.Request[currencyv1.CreateCurrencyRequest],

cmd/server/interfaces.go

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ type CurrencySvc interface {
6565
ctx context.Context,
6666
req *currencyv1.UpdateCurrencyRequest,
6767
) (*currencyv1.UpdateCurrencyResponse, error)
68+
69+
DeleteCurrency(
70+
ctx context.Context,
71+
req *currencyv1.DeleteCurrencyRequest,
72+
) (*currencyv1.DeleteCurrencyResponse, error)
6873
}
6974

7075
type DecimalSvc interface {

cmd/sync-exchange-rates/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
const (
11-
defaultExchangeRatesURL = "https://localhost/latest.json" // todo
11+
defaultExchangeRatesURL = "http://go-money-exchange-rates.s3-website.eu-north-1.amazonaws.com/latest.json"
1212
)
1313

1414
func main() {

compose/.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
TAG=2025.02.23.200442.3
22
GRPC_PORT=52055
33
PUBLIC_PORT=52055
4+
EXCHANGE_RATES_URL=http://go-money-exchange-rates.s3-website.eu-north-1.amazonaws.com/latest.json
45

56
## APP
67
DB_HOST=db

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
connectrpc.com/grpcreflect v1.3.0
88
github.com/cockroachdb/errors v1.11.3
99
github.com/dgrijalva/jwt-go v3.2.0+incompatible
10-
github.com/ft-t/go-money-pb v0.0.0-20250225190317-01b85e9aeaa8
10+
github.com/ft-t/go-money-pb v0.0.0-20250226173750-1f579486ce8b
1111
github.com/go-gormigrate/gormigrate/v2 v2.1.3
1212
github.com/golang-jwt/jwt/v5 v5.2.1
1313
github.com/golang/mock v1.6.0

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
1515
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1616
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
1717
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
18-
github.com/ft-t/go-money-pb v0.0.0-20250225190317-01b85e9aeaa8 h1:5PI1tx4EYjgZKWCHhdQbvEJXPg3PKKN2BI4ByVzBVMA=
19-
github.com/ft-t/go-money-pb v0.0.0-20250225190317-01b85e9aeaa8/go.mod h1:PI8ulxDJ0HamtFdQhvfOTJDla0blE6tJX9EuMw1IrzU=
18+
github.com/ft-t/go-money-pb v0.0.0-20250226173750-1f579486ce8b h1:wqW7BDc9ntoIS+ZQSfnlyAkr10+8wvn0kjoB2/1wOq4=
19+
github.com/ft-t/go-money-pb v0.0.0-20250226173750-1f579486ce8b/go.mod h1:PI8ulxDJ0HamtFdQhvfOTJDla0blE6tJX9EuMw1IrzU=
2020
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
2121
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
2222
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=

pkg/configuration/types.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package configuration
33
import "github.com/ft-t/go-money/pkg/boilerplate"
44

55
type Configuration struct {
6-
Db boilerplate.DbConfig `env:", prefix=DB_"`
7-
ReadOnlyDb boilerplate.DbConfig `env:", prefix=READONLY_DB_"`
8-
GrpcPort int `env:"GRPC_PORT, default=52055"`
9-
JwtPrivateKey string `env:"JWT_PRIVATE_KEY"`
6+
Db boilerplate.DbConfig `env:", prefix=DB_"`
7+
ReadOnlyDb boilerplate.DbConfig `env:", prefix=READONLY_DB_"`
8+
GrpcPort int `env:"GRPC_PORT, default=52055"`
9+
JwtPrivateKey string `env:"JWT_PRIVATE_KEY"`
10+
ExchangeRatesUrl string `env:"EXCHANGE_RATES_URL"`
1011
}

pkg/currency/converter_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package currency_test
2+
3+
import (
4+
"context"
5+
"github.com/ft-t/go-money/pkg/currency"
6+
"github.com/ft-t/go-money/pkg/database"
7+
"github.com/ft-t/go-money/pkg/testingutils"
8+
"github.com/shopspring/decimal"
9+
"github.com/stretchr/testify/assert"
10+
"testing"
11+
)
12+
13+
func TestConvert(t *testing.T) {
14+
assert.NoError(t, testingutils.FlushAllTables(cfg.Db))
15+
16+
usd := &database.Currency{
17+
ID: "USD",
18+
DecimalPlaces: 2,
19+
Rate: decimal.RequireFromString("1.0"),
20+
}
21+
assert.NoError(t, gormDB.Create(usd).Error)
22+
23+
pln := &database.Currency{
24+
ID: "PLN",
25+
DecimalPlaces: 2,
26+
Rate: decimal.RequireFromString("3.8"),
27+
}
28+
assert.NoError(t, gormDB.Create(pln).Error)
29+
30+
eur := &database.Currency{
31+
ID: "EUR",
32+
DecimalPlaces: 2,
33+
Rate: decimal.RequireFromString("0.8"),
34+
}
35+
assert.NoError(t, gormDB.Create(eur).Error)
36+
37+
cv := currency.NewConverter()
38+
39+
t.Run("USD -> PLN", func(t *testing.T) {
40+
resp, err := cv.Convert(
41+
context.TODO(),
42+
"USD",
43+
"PLN",
44+
decimal.RequireFromString("3.0"),
45+
)
46+
47+
assert.NoError(t, err)
48+
assert.EqualValues(t, "11.40", resp.StringFixed(2))
49+
})
50+
51+
t.Run("PLN -> USD -> EUR", func(t *testing.T) {
52+
resp, err := cv.Convert(
53+
context.TODO(),
54+
"PLN",
55+
"EUR",
56+
decimal.RequireFromString("4.0"),
57+
)
58+
59+
assert.NoError(t, err)
60+
assert.EqualValues(t, "0.84", resp.StringFixed(2))
61+
})
62+
63+
t.Run("USD -> XYZ", func(t *testing.T) {
64+
resp, err := cv.Convert(
65+
context.TODO(),
66+
"USD",
67+
"XYZ",
68+
decimal.RequireFromString("3.0"),
69+
)
70+
71+
assert.ErrorContains(t, err, "rate for XYZ not found")
72+
assert.EqualValues(t, decimal.Zero, resp)
73+
})
74+
}
75+
76+
func TestMissingBase(t *testing.T) {
77+
assert.NoError(t, testingutils.FlushAllTables(cfg.Db))
78+
79+
cv := currency.NewConverter()
80+
81+
resp, err := cv.Convert(
82+
context.TODO(),
83+
"USD",
84+
"PLN",
85+
decimal.RequireFromString("3.0"),
86+
)
87+
88+
assert.ErrorContains(t, err, "rate for USD not found")
89+
assert.EqualValues(t, decimal.Zero, resp)
90+
}

pkg/currency/interfaces.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package currency
2+
3+
import "net/http"
4+
5+
//go:generate mockgen -destination interfaces_mocks_test.go -package currency_test -source=interfaces.go
6+
7+
type httpClient interface {
8+
Do(req *http.Request) (*http.Response, error)
9+
}

pkg/currency/service.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ func NewService() *Service {
1818
return &Service{}
1919
}
2020

21+
func (s *Service) DeleteCurrency(
22+
ctx context.Context,
23+
req *currencyv1.DeleteCurrencyRequest,
24+
) (*currencyv1.DeleteCurrencyResponse, error) {
25+
db := database.FromContext(ctx, database.GetDb(database.DbTypeMaster))
26+
27+
var currency database.Currency
28+
if err := db.Where("id = ?", req.Id).First(&currency).Error; err != nil {
29+
return nil, err
30+
}
31+
32+
if err := db.Delete(&currency).Error; err != nil {
33+
return nil, err
34+
}
35+
36+
return &currencyv1.DeleteCurrencyResponse{
37+
Currency: s.mapCurrency(&currency),
38+
}, nil
39+
}
40+
2141
func (s *Service) GetCurrencies(
2242
ctx context.Context,
2343
_ *currencyv1.GetCurrenciesRequest,
@@ -77,18 +97,18 @@ func (s *Service) UpdateCurrency(
7797
db := database.FromContext(ctx, database.GetDb(database.DbTypeMaster))
7898

7999
var currency database.Currency
80-
if err := db.Where("id = ?", req.Currency.Id).First(&currency).Error; err != nil {
100+
if err := db.Where("id = ?", req.Id).First(&currency).Error; err != nil {
81101
return nil, err
82102
}
83103

84-
rate, err := decimal.NewFromString(req.Currency.Rate)
104+
rate, err := decimal.NewFromString(req.Rate)
85105
if err != nil {
86106
return nil, err
87107
}
88108

89109
currency.Rate = rate
90-
currency.IsActive = req.Currency.IsActive
91-
currency.DecimalPlaces = req.Currency.DecimalPlaces
110+
currency.IsActive = req.IsActive
111+
currency.DecimalPlaces = req.DecimalPlaces
92112
currency.UpdatedAt = time.Now().UTC()
93113
currency.DeletedAt = gorm.DeletedAt{
94114
Valid: false,

pkg/currency/service_test.go

+63-18
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,10 @@ func TestUpdateCurrency(t *testing.T) {
147147

148148
srv := currency.NewService()
149149
resp, err := srv.UpdateCurrency(context.TODO(), &currencyv1.UpdateCurrencyRequest{
150-
Currency: &v1.Currency{
151-
Id: "USD",
152-
Rate: "5.21",
153-
IsActive: false,
154-
DecimalPlaces: 2,
155-
},
150+
Id: "USD",
151+
Rate: "5.21",
152+
IsActive: false,
153+
DecimalPlaces: 2,
156154
})
157155

158156
assert.NoError(t, err)
@@ -172,12 +170,10 @@ func TestUpdateCurrency(t *testing.T) {
172170

173171
srv := currency.NewService()
174172
resp, err := srv.UpdateCurrency(context.TODO(), &currencyv1.UpdateCurrencyRequest{
175-
Currency: &v1.Currency{
176-
Id: "USD",
177-
Rate: "5.21",
178-
IsActive: false,
179-
DecimalPlaces: 2,
180-
},
173+
Id: "USD",
174+
Rate: "5.21",
175+
IsActive: false,
176+
DecimalPlaces: 2,
181177
})
182178

183179
assert.ErrorContains(t, err, "record not found")
@@ -198,15 +194,64 @@ func TestUpdateCurrency(t *testing.T) {
198194

199195
srv := currency.NewService()
200196
resp, err := srv.UpdateCurrency(context.TODO(), &currencyv1.UpdateCurrencyRequest{
201-
Currency: &v1.Currency{
202-
Id: "USD",
203-
Rate: "x5.21",
204-
IsActive: false,
205-
DecimalPlaces: 2,
206-
},
197+
Id: "USD",
198+
Rate: "x5.21",
199+
IsActive: false,
200+
DecimalPlaces: 2,
207201
})
208202

209203
assert.ErrorContains(t, err, "can't convert x5.21 to decimal")
210204
assert.Nil(t, resp)
211205
})
212206
}
207+
208+
func TestDeleteCurrency(t *testing.T) {
209+
t.Run("success", func(t *testing.T) {
210+
assert.NoError(t, testingutils.FlushAllTables(cfg.Db))
211+
212+
cur := &database.Currency{
213+
ID: "USD",
214+
Rate: decimal.NewFromInt(2),
215+
IsActive: true,
216+
DecimalPlaces: 4,
217+
}
218+
assert.NoError(t, gormDB.Create(cur).Error)
219+
220+
srv := currency.NewService()
221+
222+
resp, err := srv.DeleteCurrency(context.TODO(), &currencyv1.DeleteCurrencyRequest{
223+
Id: "USD",
224+
})
225+
assert.NoError(t, err)
226+
assert.NotNil(t, resp)
227+
228+
var cur2 database.Currency
229+
assert.NoError(t, gormDB.Unscoped().Where("id = ?", "USD").First(&cur2).Error)
230+
231+
assert.True(t, cur2.DeletedAt.Valid)
232+
})
233+
234+
t.Run("already deleted", func(t *testing.T) {
235+
assert.NoError(t, testingutils.FlushAllTables(cfg.Db))
236+
237+
cur := &database.Currency{
238+
ID: "USD",
239+
Rate: decimal.NewFromInt(2),
240+
IsActive: true,
241+
DecimalPlaces: 4,
242+
DeletedAt: gorm.DeletedAt{
243+
Time: time.Now(),
244+
Valid: true,
245+
},
246+
}
247+
assert.NoError(t, gormDB.Create(cur).Error)
248+
249+
srv := currency.NewService()
250+
251+
resp, err := srv.DeleteCurrency(context.TODO(), &currencyv1.DeleteCurrencyRequest{
252+
Id: "USD",
253+
})
254+
assert.ErrorContains(t, err, "record not found")
255+
assert.Nil(t, resp)
256+
})
257+
}

0 commit comments

Comments
 (0)