Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type Service interface {
RemoveAccountFromPool(ctx context.Context, poolID string, accountID string) error
ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error)
GetPool(ctx context.Context, poolID string) (*models.Pool, error)
GetPoolBalance(ctx context.Context, poolID string, atTime string) (*service.GetPoolBalanceResponse, error)
GetPoolBalance(ctx context.Context, poolID string) (*service.GetPoolBalanceResponse, error)
GetPoolBalanceAt(ctx context.Context, poolID string, atTime string) (*service.GetPoolBalanceResponse, error)
DeletePool(ctx context.Context, poolID string) error
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 49 additions & 1 deletion components/payments/cmd/api/internal/api/pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/bun/bunpaginate"
"github.com/formancehq/stack/libs/go-libs/pointer"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -297,7 +298,7 @@ func getPoolBalances(b backend.Backend) http.HandlerFunc {

span.SetAttributes(attribute.String("request.atTime", atTime))

balance, err := b.GetService().GetPoolBalance(ctx, poolID, atTime)
balance, err := b.GetService().GetPoolBalanceAt(ctx, poolID, atTime)
if err != nil {
otel.RecordError(span, err)
handleServiceErrors(w, r, err)
Expand Down Expand Up @@ -326,6 +327,53 @@ func getPoolBalances(b backend.Backend) http.HandlerFunc {
}
}

func getPoolBalancesLatest(backend backend.Backend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer().Start(r.Context(), "getPoolBalancesLatest")
defer span.End()

poolID, ok := mux.Vars(r)["poolID"]
if !ok {
var err = errors.New("missing poolID")
otel.RecordError(span, err)
api.BadRequest(w, ErrInvalidID, err)
return
}

span.SetAttributes(attribute.String("poolID", poolID))
id, err := uuid.Parse(poolID)
if err != nil {
otel.RecordError(span, err)
api.BadRequest(w, ErrInvalidID, err)
return
}

res, err := backend.GetService().GetPoolBalance(ctx, id.String())
if err != nil {
otel.RecordError(span, err)
handleServiceErrors(w, r, err)
return
}

balances := make([]*poolBalanceResponse, len(res.Balances))
for i := range res.Balances {
balances[i] = &poolBalanceResponse{
Amount: res.Balances[i].Amount,
Asset: res.Balances[i].Asset,
}
}

err = json.NewEncoder(w).Encode(api.BaseResponse[[]*poolBalanceResponse]{
Data: &balances,
})
if err != nil {
otel.RecordError(span, err)
api.InternalServerError(w, r, err)
return
}
}
}

func deletePoolHandler(b backend.Backend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer().Start(r.Context(), "deletePoolHandler")
Expand Down
118 changes: 116 additions & 2 deletions components/payments/cmd/api/internal/api/pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -911,12 +911,12 @@ func TestGetPoolBalance(t *testing.T) {
backend, mockService := newTestingBackend(t)
if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 {
mockService.EXPECT().
GetPoolBalance(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)).
GetPoolBalanceAt(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)).
Return(getPoolBalanceResponse, nil)
}
if testCase.serviceError != nil {
mockService.EXPECT().
GetPoolBalance(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)).
GetPoolBalanceAt(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)).
Return(nil, testCase.serviceError)
}

Expand All @@ -940,7 +940,121 @@ func TestGetPoolBalance(t *testing.T) {
}
})
}
}

func TestGetPoolBalanceLatest(t *testing.T) {
t.Parallel()

uuid1 := uuid.New()
type testCase struct {
name string
queryParams url.Values
poolID string
serviceError error
expectedStatusCode int
expectedErrorCode string
}

testCases := []testCase{
{
name: "nominal",
poolID: uuid1.String(),
},
{
name: "err validation from backend",
poolID: uuid1.String(),
serviceError: service.ErrValidation,
expectedStatusCode: http.StatusBadRequest,
expectedErrorCode: ErrValidation,
},
{
name: "ErrNotFound from storage",
poolID: uuid1.String(),
serviceError: storage.ErrNotFound,
expectedStatusCode: http.StatusNotFound,
expectedErrorCode: ErrNotFound,
},
{
name: "ErrDuplicateKeyValue from storage",
poolID: uuid1.String(),
serviceError: storage.ErrDuplicateKeyValue,
expectedStatusCode: http.StatusBadRequest,
expectedErrorCode: ErrUniqueReference,
},
{
name: "other storage errors from storage",
poolID: uuid1.String(),
serviceError: errors.New("some error"),
expectedStatusCode: http.StatusInternalServerError,
expectedErrorCode: sharedapi.ErrorInternal,
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

if testCase.expectedStatusCode == 0 {
testCase.expectedStatusCode = http.StatusOK
}

getPoolBalanceResponse := &service.GetPoolBalanceResponse{
Balances: []*service.Balance{
{
Amount: big.NewInt(100),
Asset: "EUR/2",
},
{
Amount: big.NewInt(12000),
Asset: "USD/2",
},
},
}

expectedPoolBalancesResponse := []*poolBalanceResponse{
{
Amount: getPoolBalanceResponse.Balances[0].Amount,
Asset: getPoolBalanceResponse.Balances[0].Asset,
},
{
Amount: getPoolBalanceResponse.Balances[1].Amount,
Asset: getPoolBalanceResponse.Balances[1].Asset,
},
}

backend, mockService := newTestingBackend(t)
if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 {
mockService.EXPECT().
GetPoolBalance(gomock.Any(), testCase.poolID).
Return(getPoolBalanceResponse, nil)
}
if testCase.serviceError != nil {
mockService.EXPECT().
GetPoolBalance(gomock.Any(), testCase.poolID).
Return(nil, testCase.serviceError)
}

router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{}, auth.NewNoAuth())

req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pools/%s/balances/latest", testCase.poolID), nil)
rec := httptest.NewRecorder()
req.URL.RawQuery = testCase.queryParams.Encode()

router.ServeHTTP(rec, req)

require.Equal(t, testCase.expectedStatusCode, rec.Code)
if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 {
var resp sharedapi.BaseResponse[[]*poolBalanceResponse]
sharedapi.Decode(t, rec.Body, &resp)
require.Equal(t, &expectedPoolBalancesResponse, resp.Data)
} else {
err := sharedapi.ErrorResponse{}
sharedapi.Decode(t, rec.Body, &err)
require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode)
}
})
}
}

func TestDeletePool(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions components/payments/cmd/api/internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func httpRouter(
authGroup.Path("/pools/{poolID}/accounts").Methods(http.MethodPost).Handler(addAccountToPoolHandler(b))
authGroup.Path("/pools/{poolID}/accounts/{accountID}").Methods(http.MethodDelete).Handler(removeAccountFromPoolHandler(b))
authGroup.Path("/pools/{poolID}/balances").Methods(http.MethodGet).Handler(getPoolBalances(b))
authGroup.Path("/pools/{poolID}/balances/latest").Methods(http.MethodGet).Handler(getPoolBalancesLatest(b))

return rootMux
}
47 changes: 46 additions & 1 deletion components/payments/cmd/api/internal/api/service/pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ type Balance struct {
Asset string
}

func (s *Service) GetPoolBalance(
func (s *Service) GetPoolBalanceAt(
ctx context.Context,
poolID string,
atTime string,
Expand Down Expand Up @@ -236,6 +236,51 @@ func (s *Service) GetPoolBalance(
}, nil
}

func (s *Service) GetPoolBalance(
ctx context.Context,
poolID string,
) (*GetPoolBalanceResponse, error) {
id, err := uuid.Parse(poolID)
if err != nil {
return nil, errors.Wrap(ErrValidation, err.Error())
}

pool, err := s.store.GetPool(ctx, id)
if err != nil {
return nil, newStorageError(err, "getting pool")
}

res := make(map[string]*big.Int)
for _, poolAccount := range pool.PoolAccounts {
balances, err := s.store.GetBalancesLatest(ctx, poolAccount.AccountID)
if err != nil {
return nil, newStorageError(err, "getting balances")
}

for _, balance := range balances {
amount, ok := res[balance.Asset.String()]
if !ok {
amount = big.NewInt(0)
}

amount.Add(amount, balance.Balance)
res[balance.Asset.String()] = amount
}
}

balances := make([]*Balance, 0, len(res))
for asset, amount := range res {
balances = append(balances, &Balance{
Asset: asset,
Amount: amount,
})
}

return &GetPoolBalanceResponse{
Balances: balances,
}, nil
}

func (s *Service) DeletePool(
ctx context.Context,
poolID string,
Expand Down
55 changes: 53 additions & 2 deletions components/payments/cmd/api/internal/api/service/pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func TestDeletePool(t *testing.T) {

}

func TestGetPoolBalance(t *testing.T) {
func TestGetPoolBalanceAt(t *testing.T) {
t.Parallel()

type testCase struct {
Expand Down Expand Up @@ -331,7 +331,58 @@ func TestGetPoolBalance(t *testing.T) {
"USD/2": big.NewInt(300),
}

balances, err := service.GetPoolBalance(context.Background(), tc.poolID, tc.atTime)
balances, err := service.GetPoolBalanceAt(context.Background(), tc.poolID, tc.atTime)
if tc.expectedError != nil {
require.True(t, errors.Is(err, tc.expectedError))
} else {
require.NoError(t, err)

require.Equal(t, 2, len(balances.Balances))
for _, balance := range balances.Balances {
expectedAmount, ok := expectedResponseMap[balance.Asset]
require.True(t, ok)
require.Equal(t, expectedAmount, balance.Amount)
}
}
})
}
}

func TestGetPoolBalance(t *testing.T) {
t.Parallel()

type testCase struct {
name string
poolID string
expectedError error
}

uuid1 := uuid.New()

testCases := []testCase{
{
name: "nominal",
poolID: uuid1.String(),
},
{
name: "invalid poolID",
poolID: "invalid",
expectedError: ErrValidation,
},
}

service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages(""))
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

expectedResponseMap := map[string]*big.Int{
"EUR/2": big.NewInt(200),
"USD/2": big.NewInt(300),
}

balances, err := service.GetPoolBalance(context.Background(), tc.poolID)
if tc.expectedError != nil {
require.True(t, errors.Is(err, tc.expectedError))
} else {
Expand Down
Loading