diff --git a/OpenApi.yml b/OpenApi.yml index e054c09b..d38b871f 100644 --- a/OpenApi.yml +++ b/OpenApi.yml @@ -684,6 +684,38 @@ components: rsk: type: string type: object + SummaryData: + properties: + acceptedQuotesCount: + type: integer + lpEarnings: + $ref: '#/components/schemas/Wei' + paidQuotesAmount: + $ref: '#/components/schemas/Wei' + paidQuotesCount: + type: integer + refundedQuotesCount: + type: integer + totalAcceptedQuotedAmount: + $ref: '#/components/schemas/Wei' + totalFeesCollected: + $ref: '#/components/schemas/Wei' + totalPenaltyAmount: + $ref: '#/components/schemas/Wei' + totalQuotesCount: + type: integer + type: object + SummaryResult: + properties: + peginSummary: + $ref: '#/components/schemas/SummaryData' + type: object + pegoutSummary: + $ref: '#/components/schemas/SummaryData' + type: object + type: object + Wei: {} + entities.Wei: {} pkg.AcceptQuoteRequest: properties: quoteHash: @@ -1034,6 +1066,34 @@ paths: "204": description: "" summary: Withdraw PegIn Collateral + /report/summaries: + get: + description: ' Returns financial data for a given period' + parameters: + - description: Start date in YYYY-MM-DD format + in: query + name: startDate + required: true + schema: + description: Start date in YYYY-MM-DD format + format: string + type: string + - description: End date in YYYY-MM-DD format + in: query + name: endDate + required: true + schema: + description: End date in YYYY-MM-DD format + format: string + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SummaryResult' + description: Financial data for the given period + summary: Summaries /reports/pegin: get: description: ' Get the last pegins on the API. Included in the management API.' diff --git a/internal/adapters/dataproviders/database/mongo/pegin.go b/internal/adapters/dataproviders/database/mongo/pegin.go index b868b088..a1976f28 100644 --- a/internal/adapters/dataproviders/database/mongo/pegin.go +++ b/internal/adapters/dataproviders/database/mongo/pegin.go @@ -4,13 +4,15 @@ import ( "context" "errors" "fmt" + "time" + "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" "github.com/rsksmart/liquidity-provider-server/internal/usecases" log "github.com/sirupsen/logrus" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" - "time" + "go.mongodb.org/mongo-driver/mongo/options" ) const ( @@ -251,3 +253,55 @@ func (repo *peginMongoRepository) DeleteQuotes(ctx context.Context, quotes []str } return uint(peginResult.DeletedCount + retainedResult.DeletedCount + creationDataResult.DeletedCount), nil } + +func (repo *peginMongoRepository) ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]quote.PeginQuoteWithRetained, error) { + result := make([]quote.PeginQuoteWithRetained, 0) + dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout) + defer cancel() + quoteFilter := bson.D{{Key: "agreement_timestamp", Value: bson.D{ + {Key: "$gte", Value: startDate.Unix()}, + {Key: "$lte", Value: endDate.Unix()}, + }}} + findOpts := options.Find().SetSort(bson.D{{Key: "agreement_timestamp", Value: 1}}) + quoteCursor, err := repo.conn.Collection(PeginQuoteCollection).Find(dbCtx, quoteFilter, findOpts) + if err != nil { + return nil, err + } + var storedQuotes []StoredPeginQuote + if err = quoteCursor.All(dbCtx, &storedQuotes); err != nil { + return nil, err + } + if len(storedQuotes) == 0 { + logDbInteraction(Read, result) + return result, nil + } + hashToIndex := make(map[string]int, len(storedQuotes)) + quoteHashes := make([]string, len(storedQuotes)) + result = make([]quote.PeginQuoteWithRetained, len(storedQuotes)) + for i, stored := range storedQuotes { + quoteHashes[i] = stored.Hash + hashToIndex[stored.Hash] = i + result[i] = quote.PeginQuoteWithRetained{ + Quote: stored.PeginQuote, + RetainedQuote: quote.RetainedPeginQuote{}, + } + } + retainedCursor, err := repo.conn.Collection(RetainedPeginQuoteCollection).Find( + dbCtx, + bson.D{{Key: "quote_hash", Value: bson.D{{Key: "$in", Value: quoteHashes}}}}, + ) + if err != nil { + return result, err + } + var retainedQuotes []quote.RetainedPeginQuote + if err = retainedCursor.All(dbCtx, &retainedQuotes); err != nil { + return result, err + } + for _, retainedQuote := range retainedQuotes { + if idx, exists := hashToIndex[retainedQuote.QuoteHash]; exists { + result[idx].RetainedQuote = retainedQuote + } + } + logDbInteraction(Read, len(result)) + return result, nil +} diff --git a/internal/adapters/dataproviders/database/mongo/pegout.go b/internal/adapters/dataproviders/database/mongo/pegout.go index a40d9b69..6a3bec7f 100644 --- a/internal/adapters/dataproviders/database/mongo/pegout.go +++ b/internal/adapters/dataproviders/database/mongo/pegout.go @@ -4,6 +4,9 @@ import ( "context" "errors" "fmt" + "regexp" + "time" + "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" "github.com/rsksmart/liquidity-provider-server/internal/usecases" log "github.com/sirupsen/logrus" @@ -11,8 +14,6 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "regexp" - "time" ) const ( @@ -360,3 +361,55 @@ func (repo *pegoutMongoRepository) UpsertPegoutDeposits(ctx context.Context, dep } return err } + +func (repo *pegoutMongoRepository) ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]quote.PegoutQuoteWithRetained, error) { + result := make([]quote.PegoutQuoteWithRetained, 0) + dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout) + defer cancel() + quoteFilter := bson.D{{Key: "agreement_timestamp", Value: bson.D{ + {Key: "$gte", Value: startDate.Unix()}, + {Key: "$lte", Value: endDate.Unix()}, + }}} + findOpts := options.Find().SetSort(bson.D{{Key: "agreement_timestamp", Value: 1}}) + quoteCursor, err := repo.conn.Collection(PegoutQuoteCollection).Find(dbCtx, quoteFilter, findOpts) + if err != nil { + return nil, err + } + var storedQuotes []StoredPegoutQuote + if err = quoteCursor.All(dbCtx, &storedQuotes); err != nil { + return nil, err + } + if len(storedQuotes) == 0 { + logDbInteraction(Read, result) + return result, nil + } + hashToIndex := make(map[string]int, len(storedQuotes)) + quoteHashes := make([]string, len(storedQuotes)) + result = make([]quote.PegoutQuoteWithRetained, len(storedQuotes)) + for i, stored := range storedQuotes { + quoteHashes[i] = stored.Hash + hashToIndex[stored.Hash] = i + result[i] = quote.PegoutQuoteWithRetained{ + Quote: stored.PegoutQuote, + RetainedQuote: quote.RetainedPegoutQuote{}, + } + } + retainedCursor, err := repo.conn.Collection(RetainedPegoutQuoteCollection).Find( + dbCtx, + bson.D{{Key: "quote_hash", Value: bson.D{{Key: "$in", Value: quoteHashes}}}}, + ) + if err != nil { + return result, err + } + var retainedQuotes []quote.RetainedPegoutQuote + if err = retainedCursor.All(dbCtx, &retainedQuotes); err != nil { + return result, err + } + for _, retainedQuote := range retainedQuotes { + if idx, exists := hashToIndex[retainedQuote.QuoteHash]; exists { + result[idx].RetainedQuote = retainedQuote + } + } + logDbInteraction(Read, len(result)) + return result, nil +} diff --git a/internal/adapters/entrypoints/rest/assets/management.html b/internal/adapters/entrypoints/rest/assets/management.html index b40a7200..fbb44760 100644 --- a/internal/adapters/entrypoints/rest/assets/management.html +++ b/internal/adapters/entrypoints/rest/assets/management.html @@ -118,6 +118,51 @@
Current Configuration
+
+
+
+
Reports
+
+
Financial Summaries
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+
diff --git a/internal/adapters/entrypoints/rest/assets/static/management.css b/internal/adapters/entrypoints/rest/assets/static/management.css index 7ca39de2..f9579278 100644 --- a/internal/adapters/entrypoints/rest/assets/static/management.css +++ b/internal/adapters/entrypoints/rest/assets/static/management.css @@ -46,3 +46,17 @@ pre { 50% { width: 50%; } 100% { width: 100%; } } + +#summariesResult { + margin-top: 20px; +} + +#peginSummary table, +#pegoutSummary table { + margin-bottom: 0; +} + +#peginSummary th, +#pegoutSummary th { + width: 60%; +} diff --git a/internal/adapters/entrypoints/rest/assets/static/management.js b/internal/adapters/entrypoints/rest/assets/static/management.js index d7418dc3..c3e7377b 100644 --- a/internal/adapters/entrypoints/rest/assets/static/management.js +++ b/internal/adapters/entrypoints/rest/assets/static/management.js @@ -572,6 +572,62 @@ const addCollateral = async (amountId, endpoint, elementId, loadingBarId, button } }; +const displaySummaryData = (container, data) => { + container.innerHTML = ''; + const table = document.createElement('table'); + table.classList.add('table', 'table-striped'); + const rows = [ + { label: 'Total Quotes', value: data.totalQuotesCount }, + { label: 'Accepted Quotes', value: data.acceptedQuotesCount }, + { label: 'Paid Quotes', value: data.paidQuotesCount }, + { label: 'Paid Quotes Amount', value: data.paidQuotesAmount }, + { label: 'Total Accepted Amount', value: data.totalAcceptedQuotedAmount }, + { label: 'Total Fees Collected', value: data.totalFeesCollected }, + { label: 'Refunded Quotes', value: data.refundedQuotesCount }, + { label: 'Total Penalty Amount', value: data.totalPenaltyAmount }, + { label: 'LP Earnings', value: data.lpEarnings } + ]; + rows.forEach(row => { + const tr = document.createElement('tr'); + const th = document.createElement('th'); + th.textContent = row.label; + const td = document.createElement('td'); + td.textContent = row.value; + tr.appendChild(th); + tr.appendChild(td); + table.appendChild(tr); + }); + container.appendChild(table); +}; + +const fetchSummariesReport = async (csrfToken) => { + const startDate = document.getElementById('summaryStartDate').value; + const endDate = document.getElementById('summaryEndDate').value; + if (!startDate || !endDate) { + showErrorToast('Please select both start and end dates'); + return; + } + try { + const response = await fetch(`/report/summaries?startDate=${startDate}&endDate=${endDate}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken + } + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to fetch summaries'); + } + const data = await response.json(); + document.getElementById('summariesResult').style.display = 'block'; + displaySummaryData(document.getElementById('peginSummary'), data.peginSummary); + displaySummaryData(document.getElementById('pegoutSummary'), data.pegoutSummary); + } catch (error) { + showErrorToast(`Error fetching summaries: ${error.message}`); + } +}; + document.addEventListener('DOMContentLoaded', () => { const csrfToken = data.CsrfToken; const configurations = data.Configuration; @@ -582,6 +638,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('addPeginCollateralButton').addEventListener('click', () => addCollateral('addPeginCollateralAmount', '/pegin/addCollateral', 'peginCollateral', 'peginLoadingBar', 'addPeginCollateralButton', csrfToken)); document.getElementById('addPegoutCollateralButton').addEventListener('click', () => addCollateral('addPegoutCollateralAmount', '/pegout/addCollateral', 'pegoutCollateral', 'pegoutLoadingBar', 'addPegoutCollateralButton', csrfToken)); document.getElementById('saveConfig').addEventListener('click', () => saveConfig(csrfToken, configurations)); + document.getElementById('fetchSummariesButton').addEventListener('click', () => fetchSummariesReport(csrfToken)); populateConfigSection('generalConfig', configurations.general); populateConfigSection('peginConfig', configurations.pegin); @@ -591,4 +648,11 @@ document.addEventListener('DOMContentLoaded', () => { fetchData('/pegin/collateral', 'peginCollateral', csrfToken); fetchData('/pegout/collateral', 'pegoutCollateral', csrfToken); checkFeeWarnings(); + + const today = new Date(); + const lastMonth = new Date(today); + lastMonth.setMonth(today.getMonth() - 1); + + document.getElementById('summaryStartDate').value = lastMonth.toISOString().split('T')[0]; + document.getElementById('summaryEndDate').value = today.toISOString().split('T')[0]; }); \ No newline at end of file diff --git a/internal/adapters/entrypoints/rest/common.go b/internal/adapters/entrypoints/rest/common.go index d6e360e3..670d4e62 100644 --- a/internal/adapters/entrypoints/rest/common.go +++ b/internal/adapters/entrypoints/rest/common.go @@ -16,11 +16,12 @@ import ( const ( HeaderContentType = "Content-Type" -) -const ( ContentTypeJson = "application/json" ContentTypeForm = "application/x-www-form-urlencoded" + + StartDateParam = "startDate" + EndDateParam = "endDate" ) var RequestValidator = validator.New(validator.WithRequiredStructEnabled()) @@ -155,3 +156,36 @@ func ValidateRequest[T any](w http.ResponseWriter, body *T) error { func RequiredQueryParam(name string) error { return fmt.Errorf("required query parameter %s is missing", name) } + +func ParseDateRange(req *http.Request, dateFormat string) (time.Time, time.Time, error) { + start := req.URL.Query().Get(StartDateParam) + end := req.URL.Query().Get(EndDateParam) + if start == "" || end == "" { + missing := []string{} + if start == "" { + missing = append(missing, StartDateParam) + } + if end == "" { + missing = append(missing, EndDateParam) + } + return time.Time{}, time.Time{}, fmt.Errorf("missing required parameters: %v", missing) + } + startDate, err := time.Parse(dateFormat, start) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid start date format: %w", err) + } + endDate, err := time.Parse(dateFormat, end) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid end date format: %w", err) + } + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, time.UTC) + return startDate, endDate, nil +} + +func ValidateDateRange(startDate, endDate time.Time, dateFormat string) error { + if endDate.Before(startDate) { + return fmt.Errorf("invalid date range: end date %s is before start date %s", + endDate.Format(dateFormat), startDate.Format(dateFormat)) + } + return nil +} diff --git a/internal/adapters/entrypoints/rest/common_test.go b/internal/adapters/entrypoints/rest/common_test.go index 5b625bcd..04b2c4b4 100644 --- a/internal/adapters/entrypoints/rest/common_test.go +++ b/internal/adapters/entrypoints/rest/common_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest" "github.com/rsksmart/liquidity-provider-server/pkg" @@ -194,7 +195,6 @@ func TestMaxDecimalPlacesValidation(t *testing.T) { {value: 1e-5, expectError: true, description: "scientific notation exceeds limit"}, {value: 1.123456789, expectError: true, description: "many decimal places"}, } - for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { ts := testStruct{Number: tc.value} @@ -207,3 +207,149 @@ func TestMaxDecimalPlacesValidation(t *testing.T) { }) } } + +func getDateRangeTestCases() []struct { //nolint:funlen + name string + queryParams map[string]string + expectedValid bool + expectedStatus int +} { + return []struct { + name string + queryParams map[string]string + expectedValid bool + expectedStatus int + }{ + { + name: "valid date range", + queryParams: map[string]string{ + "startDate": "2023-01-01", + "endDate": "2023-01-31", + }, + expectedValid: true, + expectedStatus: http.StatusOK, + }, + { + name: "missing startDate", + queryParams: map[string]string{ + "endDate": "2023-01-31", + }, + expectedValid: false, + expectedStatus: http.StatusBadRequest, + }, + { + name: "missing endDate", + queryParams: map[string]string{ + "startDate": "2023-01-01", + }, + expectedValid: false, + expectedStatus: http.StatusBadRequest, + }, + { + name: "invalid startDate format", + queryParams: map[string]string{ + "startDate": "01/01/2023", + "endDate": "2023-01-31", + }, + expectedValid: false, + expectedStatus: http.StatusBadRequest, + }, + { + name: "invalid endDate format", + queryParams: map[string]string{ + "startDate": "2023-01-01", + "endDate": "31/01/2023", + }, + expectedValid: false, + expectedStatus: http.StatusBadRequest, + }, + { + name: "endDate before startDate", + queryParams: map[string]string{ + "startDate": "2023-02-01", + "endDate": "2023-01-31", + }, + expectedValid: false, + expectedStatus: http.StatusBadRequest, + }, + } +} + +func TestParseDateRange(t *testing.T) { + dateFormat := "2006-01-02" + tests := getDateRangeTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/test", nil) + q := req.URL.Query() + for key, value := range tt.queryParams { + q.Add(key, value) + } + req.URL.RawQuery = q.Encode() + startDate, endDate, err := rest.ParseDateRange(req, dateFormat) + if tt.name == "valid_date_range" || tt.name == "endDate_before_startDate" { + require.NoError(t, err) + expectedStartDate, parseErr := time.Parse(dateFormat, tt.queryParams["startDate"]) + require.NoError(t, parseErr) + expectedEndDate, parseErr := time.Parse(dateFormat, tt.queryParams["endDate"]) + require.NoError(t, parseErr) + expectedEndDate = time.Date(expectedEndDate.Year(), expectedEndDate.Month(), expectedEndDate.Day(), 23, 59, 59, 0, time.UTC) + assert.Equal(t, expectedStartDate, startDate) + assert.Equal(t, expectedEndDate, endDate) + } else if tt.name == "missing_startDate" || tt.name == "missing_endDate" { + require.Error(t, err) + assert.Contains(t, err.Error(), "missing required parameters") + } else if tt.name == "invalid_startDate_format" { + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid start date format") + } else if tt.name == "invalid_endDate_format" { + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid end date format") + } + }) + } +} + +func TestValidateDateRange(t *testing.T) { + dateFormat := "2006-01-02" + validStart, err := time.Parse(dateFormat, "2023-01-01") + require.NoError(t, err) + validEnd, err := time.Parse(dateFormat, "2023-01-31") + require.NoError(t, err) + validEnd = time.Date(validEnd.Year(), validEnd.Month(), validEnd.Day(), 23, 59, 59, 0, time.UTC) + invalidStart, err := time.Parse(dateFormat, "2023-02-01") + require.NoError(t, err) + invalidEnd, err := time.Parse(dateFormat, "2023-01-31") + require.NoError(t, err) + invalidEnd = time.Date(invalidEnd.Year(), invalidEnd.Month(), invalidEnd.Day(), 23, 59, 59, 0, time.UTC) + tests := []struct { + name string + startDate time.Time + endDate time.Time + expectError bool + }{ + { + name: "valid_date_range", + startDate: validStart, + endDate: validEnd, + expectError: false, + }, + { + name: "end_before_start", + startDate: invalidStart, + endDate: invalidEnd, + expectError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := rest.ValidateDateRange(tt.startDate, tt.endDate, dateFormat) + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid date range") + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/adapters/entrypoints/rest/handlers/get_report_summaries.go b/internal/adapters/entrypoints/rest/handlers/get_report_summaries.go new file mode 100644 index 00000000..4abfec12 --- /dev/null +++ b/internal/adapters/entrypoints/rest/handlers/get_report_summaries.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "net/http" + + log "github.com/sirupsen/logrus" + + "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest" + "github.com/rsksmart/liquidity-provider-server/internal/usecases/reports" + "github.com/rsksmart/liquidity-provider-server/pkg" +) + +// NewGetReportSummariesHandler handles GET /report/summaries +// @Title Summaries +// @Description Returns financial data for a given period +// @Param startDate query string true "Start date in YYYY-MM-DD format" Format(date) +// @Param endDate query string true "End date in YYYY-MM-DD format" Format(date) +// @Success 200 {object} pkg.SummaryResultDTO "Financial data for the given period" +// @Router /report/summaries [get] +func NewGetReportSummariesHandler(useCase *reports.SummariesUseCase) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + startDate, endDate, err := rest.ParseDateRange(req, reports.DateFormat) + if err != nil { + log.Errorf("Error parsing date range: %v", err) + rest.JsonErrorResponse(w, http.StatusBadRequest, + rest.NewErrorResponseWithDetails("Invalid date range", rest.DetailsFromError(err), true)) + return + } + validateErr := rest.ValidateDateRange(startDate, endDate, reports.DateFormat) + if validateErr != nil { + log.Errorf("Error validating date range: %v", validateErr) + rest.JsonErrorResponse(w, http.StatusBadRequest, + rest.NewErrorResponseWithDetails("Invalid date range", rest.DetailsFromError(validateErr), true)) + return + } + response, err := useCase.Run(req.Context(), startDate, endDate) + if err != nil { + log.Errorf("Error running summaries use case: %v", err) + rest.JsonErrorResponse(w, http.StatusInternalServerError, + rest.NewErrorResponseWithDetails(UnknownErrorMessage, rest.DetailsFromError(err), false)) + return + } + dto := pkg.ToSummaryResultDTO(response) + rest.JsonResponseWithBody(w, http.StatusOK, &dto) + } +} diff --git a/internal/adapters/entrypoints/rest/handlers/get_report_summaries_test.go b/internal/adapters/entrypoints/rest/handlers/get_report_summaries_test.go new file mode 100644 index 00000000..da0e16d3 --- /dev/null +++ b/internal/adapters/entrypoints/rest/handlers/get_report_summaries_test.go @@ -0,0 +1,149 @@ +package handlers_test + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/handlers" + "github.com/rsksmart/liquidity-provider-server/internal/entities" + "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" + "github.com/rsksmart/liquidity-provider-server/internal/usecases/reports" + "github.com/rsksmart/liquidity-provider-server/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetReportSummariesHandler(t *testing.T) { //nolint:funlen + tests := []struct { + name string + url string + expectedStatus int + mockResponse reports.SummaryResult + mockErr error + setupMocks func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) + }{ + { + name: "Success with valid date range", + url: "/report/summaries?startDate=2023-01-01&endDate=2023-01-31", + expectedStatus: http.StatusOK, + mockResponse: reports.SummaryResult{ + PeginSummary: reports.SummaryData{ + TotalQuotesCount: 10, + AcceptedQuotesCount: 8, + PaidQuotesCount: 6, + PaidQuotesAmount: entities.NewWei(1000), + TotalFeesCollected: entities.NewWei(50), + RefundedQuotesCount: 2, + TotalPenaltyAmount: entities.NewWei(20), + LpEarnings: entities.NewWei(30), + }, + PegoutSummary: reports.SummaryData{ + TotalQuotesCount: 5, + AcceptedQuotesCount: 4, + PaidQuotesCount: 3, + PaidQuotesAmount: entities.NewWei(500), + TotalFeesCollected: entities.NewWei(40), + RefundedQuotesCount: 1, + TotalPenaltyAmount: entities.NewWei(0), + LpEarnings: entities.NewWei(40), + }, + }, + mockErr: nil, + setupMocks: func(t *testing.T, peginRepo *mocks.PeginQuoteRepositoryMock, pegoutRepo *mocks.PegoutQuoteRepositoryMock) { + startDate, err := time.Parse(reports.DateFormat, "2023-01-01") + require.NoError(t, err) + endDate, err := time.Parse(reports.DateFormat, "2023-01-31") + require.NoError(t, err) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, endDate.Location()) + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PeginQuoteWithRetained{}, nil) + pegoutRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PegoutQuoteWithRetained{}, nil) + }, + }, + { + name: "Missing startDate parameter", + url: "/report/summaries?endDate=2023-01-31", + expectedStatus: http.StatusBadRequest, + mockResponse: reports.SummaryResult{}, + mockErr: nil, + setupMocks: func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) {}, + }, + { + name: "Missing endDate parameter", + url: "/report/summaries?startDate=2023-01-01", + expectedStatus: http.StatusBadRequest, + mockResponse: reports.SummaryResult{}, + mockErr: nil, + setupMocks: func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) {}, + }, + { + name: "Invalid startDate format", + url: "/report/summaries?startDate=01/01/2023&endDate=2023-01-31", + expectedStatus: http.StatusBadRequest, + mockResponse: reports.SummaryResult{}, + mockErr: nil, + setupMocks: func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) {}, + }, + { + name: "Invalid endDate format", + url: "/report/summaries?startDate=2023-01-01&endDate=31/01/2023", + expectedStatus: http.StatusBadRequest, + mockResponse: reports.SummaryResult{}, + mockErr: nil, + setupMocks: func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) {}, + }, + { + name: "EndDate before StartDate", + url: "/report/summaries?startDate=2023-02-01&endDate=2023-01-31", + expectedStatus: http.StatusBadRequest, + mockResponse: reports.SummaryResult{}, + mockErr: nil, + setupMocks: func(*testing.T, *mocks.PeginQuoteRepositoryMock, *mocks.PegoutQuoteRepositoryMock) {}, + }, + { + name: "Error in use case", + url: "/report/summaries?startDate=2023-01-01&endDate=2023-01-31", + expectedStatus: http.StatusInternalServerError, + mockResponse: reports.SummaryResult{}, + mockErr: errors.New("test error"), + setupMocks: func(t *testing.T, peginRepo *mocks.PeginQuoteRepositoryMock, pegoutRepo *mocks.PegoutQuoteRepositoryMock) { + startDate, err := time.Parse(reports.DateFormat, "2023-01-01") + require.NoError(t, err) + endDate, err := time.Parse(reports.DateFormat, "2023-01-31") + require.NoError(t, err) + endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, endDate.Location()) + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PeginQuoteWithRetained{}, errors.New("test error")) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + peginRepoMock := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepoMock := mocks.NewPegoutQuoteRepositoryMock(t) + tt.setupMocks(t, peginRepoMock, pegoutRepoMock) + useCase := reports.NewSummariesUseCase(peginRepoMock, pegoutRepoMock, nil) + handler := handlers.NewGetReportSummariesHandler(useCase) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, tt.url, nil) + require.NoError(t, err) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + assert.Equal(t, tt.expectedStatus, rr.Code) + if tt.expectedStatus == http.StatusOK { + var response reports.SummaryResult + err = json.Unmarshal(rr.Body.Bytes(), &response) + require.NoError(t, err) + assert.NotNil(t, response) + } + peginRepoMock.AssertExpectations(t) + pegoutRepoMock.AssertExpectations(t) + }) + } +} diff --git a/internal/adapters/entrypoints/rest/registry/registry.go b/internal/adapters/entrypoints/rest/registry/registry.go index dedd4048..28ff1cb8 100644 --- a/internal/adapters/entrypoints/rest/registry/registry.go +++ b/internal/adapters/entrypoints/rest/registry/registry.go @@ -36,6 +36,7 @@ type UseCaseRegistry interface { GetPegoutStatusUseCase() *pegout.StatusUseCase GetAvailableLiquidityUseCase() *liquidity_provider.GetAvailableLiquidityUseCase GetServerInfoUseCase() *liquidity_provider.ServerInfoUseCase + SummariesUseCase() *reports.SummariesUseCase GetPeginReportUseCase() *reports.GetPeginReportUseCase GetPegoutReportUseCase() *reports.GetPegoutReportUseCase GetRevenueReportUseCase() *reports.GetRevenueReportUseCase diff --git a/internal/adapters/entrypoints/rest/routes/management.go b/internal/adapters/entrypoints/rest/routes/management.go index 7f697927..3d62c1e9 100644 --- a/internal/adapters/entrypoints/rest/routes/management.go +++ b/internal/adapters/entrypoints/rest/routes/management.go @@ -1,12 +1,13 @@ package routes import ( + "net/http" + "github.com/gorilla/sessions" "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/assets" "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/handlers" "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/registry" "github.com/rsksmart/liquidity-provider-server/internal/configuration/environment" - "net/http" ) const ( @@ -56,6 +57,11 @@ func GetManagementEndpoints(env environment.Environment, useCaseRegistry registr Method: http.MethodPost, Handler: handlers.NewWithdrawCollateralHandler(useCaseRegistry.WithdrawCollateralUseCase()), }, + { + Path: "/report/summaries", + Method: http.MethodGet, + Handler: handlers.NewGetReportSummariesHandler(useCaseRegistry.SummariesUseCase()), + }, { Path: "/configuration", Method: http.MethodPost, diff --git a/internal/adapters/entrypoints/rest/routes/management_test.go b/internal/adapters/entrypoints/rest/routes/management_test.go index c1253423..5cfbf8b5 100644 --- a/internal/adapters/entrypoints/rest/routes/management_test.go +++ b/internal/adapters/entrypoints/rest/routes/management_test.go @@ -1,6 +1,9 @@ package routes_test import ( + "strings" + "testing" + "github.com/rsksmart/liquidity-provider-server/internal/adapters/entrypoints/rest/routes" "github.com/rsksmart/liquidity-provider-server/internal/configuration/environment" "github.com/rsksmart/liquidity-provider-server/internal/usecases/liquidity_provider" @@ -12,8 +15,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" - "strings" - "testing" ) func TestGetManagementEndpoints(t *testing.T) { @@ -25,6 +26,7 @@ func TestGetManagementEndpoints(t *testing.T) { registryMock.EXPECT().ChangeStatusUseCase().Return(&liquidity_provider.ChangeStatusUseCase{}) registryMock.EXPECT().ResignationUseCase().Return(&liquidity_provider.ResignUseCase{}) registryMock.EXPECT().WithdrawCollateralUseCase().Return(&liquidity_provider.WithdrawCollateralUseCase{}) + registryMock.EXPECT().SummariesUseCase().Return(&reports.SummariesUseCase{}) registryMock.EXPECT().GetConfigurationUseCase().Return(&liquidity_provider.GetConfigUseCase{}) registryMock.EXPECT().SetGeneralConfigUseCase().Return(&liquidity_provider.SetGeneralConfigUseCase{}) registryMock.EXPECT().SetPeginConfigUseCase().Return(&liquidity_provider.SetPeginConfigUseCase{}) @@ -43,7 +45,7 @@ func TestGetManagementEndpoints(t *testing.T) { err := yaml.Unmarshal(specBytes, spec) require.NoError(t, err) - assert.Len(t, endpoints, 20) + assert.Len(t, endpoints, 21) for _, endpoint := range endpoints { if endpoint.Path != routes.IconPath && endpoint.Path != routes.StaticPath { lowerCaseMethod := strings.ToLower(endpoint.Method) diff --git a/internal/adapters/entrypoints/rest/routes/routes_test.go b/internal/adapters/entrypoints/rest/routes/routes_test.go index 6ebc3577..7b863a55 100644 --- a/internal/adapters/entrypoints/rest/routes/routes_test.go +++ b/internal/adapters/entrypoints/rest/routes/routes_test.go @@ -2,6 +2,11 @@ package routes_test import ( "encoding/hex" + "net/http" + "net/http/httptest" + "slices" + "testing" + "github.com/gorilla/csrf" "github.com/gorilla/mux" "github.com/gorilla/sessions" @@ -17,10 +22,6 @@ import ( "github.com/rsksmart/liquidity-provider-server/test/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/http" - "net/http/httptest" - "slices" - "testing" ) // nolint:gosec // Linter is assuming the header name is a password @@ -223,6 +224,7 @@ func setupRegistryMock(registryMock *mocks.UseCaseRegistryMock) { registryMock.EXPECT().GetPeginStatusUseCase().Return(&pegin.StatusUseCase{}) registryMock.EXPECT().GetPegoutStatusUseCase().Return(&pegout.StatusUseCase{}) registryMock.EXPECT().GetAvailableLiquidityUseCase().Return(&liquidity_provider.GetAvailableLiquidityUseCase{}) + registryMock.EXPECT().SummariesUseCase().Return(&reports.SummariesUseCase{}) registryMock.EXPECT().GetPeginCollateralUseCase().Return(&pegin.GetCollateralUseCase{}) registryMock.EXPECT().AddPeginCollateralUseCase().Return(&pegin.AddCollateralUseCase{}) diff --git a/internal/configuration/registry/usecase.go b/internal/configuration/registry/usecase.go index 8d656947..21bb002e 100644 --- a/internal/configuration/registry/usecase.go +++ b/internal/configuration/registry/usecase.go @@ -59,6 +59,7 @@ type UseCaseRegistry struct { availableLiquidityUseCase *liquidity_provider.GetAvailableLiquidityUseCase updatePeginDepositUseCase *watcher.UpdatePeginDepositUseCase getServerInfoUseCase *liquidity_provider.ServerInfoUseCase + summariesUseCase *reports.SummariesUseCase getPeginReportUseCase *reports.GetPeginReportUseCase getPegoutReportUseCase *reports.GetPegoutReportUseCase getRevenueReportUseCase *reports.GetRevenueReportUseCase @@ -76,6 +77,11 @@ func NewUseCaseRegistry( mutexes entities.ApplicationMutexes, ) *UseCaseRegistry { return &UseCaseRegistry{ + summariesUseCase: reports.NewSummariesUseCase( + databaseRegistry.PeginRepository, + databaseRegistry.PegoutRepository, + databaseRegistry.PenalizedEventRepository, + ), getPeginQuoteUseCase: pegin.NewGetQuoteUseCase( messaging.Rpc, rskRegistry.Contracts, @@ -360,6 +366,10 @@ func (registry *UseCaseRegistry) GetServerInfoUseCase() *liquidity_provider.Serv return registry.getServerInfoUseCase } +func (registry *UseCaseRegistry) SummariesUseCase() *reports.SummariesUseCase { + return registry.summariesUseCase +} + func (registry *UseCaseRegistry) GetPeginReportUseCase() *reports.GetPeginReportUseCase { return registry.getPeginReportUseCase } diff --git a/internal/entities/quote/pegin_quote.go b/internal/entities/quote/pegin_quote.go index c7e0a085..595f4973 100644 --- a/internal/entities/quote/pegin_quote.go +++ b/internal/entities/quote/pegin_quote.go @@ -2,9 +2,10 @@ package quote import ( "context" + "time" + "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/utils" - "time" ) const ( @@ -36,6 +37,12 @@ type PeginQuoteRepository interface { GetRetainedQuoteByState(ctx context.Context, states ...PeginState) ([]RetainedPeginQuote, error) // DeleteQuotes deletes both regular and retained quotes DeleteQuotes(ctx context.Context, quotes []string) (uint, error) + ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]PeginQuoteWithRetained, error) +} + +type PeginQuoteWithRetained struct { + Quote PeginQuote + RetainedQuote RetainedPeginQuote } type CreatedPeginQuote struct { diff --git a/internal/entities/quote/pegout_quote.go b/internal/entities/quote/pegout_quote.go index 51a6ab56..89c8fe5a 100644 --- a/internal/entities/quote/pegout_quote.go +++ b/internal/entities/quote/pegout_quote.go @@ -44,6 +44,7 @@ type PegoutQuoteRepository interface { DeleteQuotes(ctx context.Context, quotes []string) (uint, error) UpsertPegoutDeposit(ctx context.Context, deposit PegoutDeposit) error UpsertPegoutDeposits(ctx context.Context, deposits []PegoutDeposit) error + ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]PegoutQuoteWithRetained, error) } type CreatedPegoutQuote struct { @@ -178,3 +179,8 @@ type PegoutBtcSentToUserEvent struct { CreationData PegoutCreationData Error error } + +type PegoutQuoteWithRetained struct { + Quote PegoutQuote + RetainedQuote RetainedPegoutQuote +} diff --git a/internal/usecases/common.go b/internal/usecases/common.go index 065eb18c..f729299c 100644 --- a/internal/usecases/common.go +++ b/internal/usecases/common.go @@ -6,10 +6,11 @@ import ( "encoding/json" "errors" "fmt" + "math/big" + "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/blockchain" "github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider" - "math/big" ) // used for error logging @@ -58,6 +59,7 @@ const ( GetAvailableLiquidityId UseCaseId = "GetAvailableLiquidity" UpdatePeginDepositId UseCaseId = "UpdatePeginDeposit" ServerInfoId UseCaseId = "ServerInfo" + SummariesUseCaseId UseCaseId = "Summaries" GetPeginReportId UseCaseId = "GetPeginReport" GetPegoutReportId UseCaseId = "GetPegoutReport" GetRevenueReportId UseCaseId = "GetRevenueReport" diff --git a/internal/usecases/reports/summaries.go b/internal/usecases/reports/summaries.go new file mode 100644 index 00000000..97adc4e2 --- /dev/null +++ b/internal/usecases/reports/summaries.go @@ -0,0 +1,255 @@ +package reports + +import ( + "context" + "time" + + "github.com/rsksmart/liquidity-provider-server/internal/entities" + "github.com/rsksmart/liquidity-provider-server/internal/entities/penalization" + "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" + "github.com/rsksmart/liquidity-provider-server/internal/usecases" +) + +const ( + DateFormat = time.DateOnly +) + +type SummaryResult struct { + PeginSummary SummaryData `json:"peginSummary"` + PegoutSummary SummaryData `json:"pegoutSummary"` +} + +type SummaryData struct { + TotalQuotesCount int64 `json:"totalQuotesCount"` + AcceptedQuotesCount int64 `json:"acceptedQuotesCount"` + PaidQuotesCount int64 `json:"paidQuotesCount"` + PaidQuotesAmount *entities.Wei `json:"paidQuotesAmount"` + TotalAcceptedQuotedAmount *entities.Wei `json:"totalAcceptedQuotedAmount"` + TotalFeesCollected *entities.Wei `json:"totalFeesCollected"` + RefundedQuotesCount int64 `json:"refundedQuotesCount"` + TotalPenaltyAmount *entities.Wei `json:"totalPenaltyAmount"` + LpEarnings *entities.Wei `json:"lpEarnings"` +} + +type summaryTotals struct { + AcceptedTotalAmount *entities.Wei + TotalFees *entities.Wei + CallFees *entities.Wei + TotalPenalty *entities.Wei +} + +func newSummaryTotals() *summaryTotals { + return &summaryTotals{ + AcceptedTotalAmount: entities.NewWei(0), + TotalFees: entities.NewWei(0), + CallFees: entities.NewWei(0), + TotalPenalty: entities.NewWei(0), + } +} + +type SummariesUseCase struct { + peginRepo quote.PeginQuoteRepository + pegoutRepo quote.PegoutQuoteRepository + penalizedRepo penalization.PenalizedEventRepository +} + +func NewSummaryData() SummaryData { + return SummaryData{ + TotalQuotesCount: 0, + AcceptedQuotesCount: 0, + PaidQuotesCount: 0, + PaidQuotesAmount: entities.NewWei(0), + TotalAcceptedQuotedAmount: entities.NewWei(0), + TotalFeesCollected: entities.NewWei(0), + RefundedQuotesCount: 0, + TotalPenaltyAmount: entities.NewWei(0), + LpEarnings: entities.NewWei(0), + } +} + +func NewSummariesUseCase( + peginRepo quote.PeginQuoteRepository, + pegoutRepo quote.PegoutQuoteRepository, + penalizedRepo penalization.PenalizedEventRepository, +) *SummariesUseCase { + return &SummariesUseCase{peginRepo: peginRepo, pegoutRepo: pegoutRepo, penalizedRepo: penalizedRepo} +} + +func (u *SummariesUseCase) Run(ctx context.Context, startDate, endDate time.Time) (SummaryResult, error) { + peginData, err := u.aggregatePeginData(ctx, startDate, endDate) + if err != nil { + return SummaryResult{}, usecases.WrapUseCaseError(usecases.SummariesUseCaseId, err) + } + pegoutData, err := u.aggregatePegoutData(ctx, startDate, endDate) + if err != nil { + return SummaryResult{}, usecases.WrapUseCaseError(usecases.SummariesUseCaseId, err) + } + return SummaryResult{PeginSummary: peginData, PegoutSummary: pegoutData}, nil +} + +func (u *SummariesUseCase) buildPenalizedMap(ctx context.Context, quoteHashes []string) (map[string]*entities.Wei, error) { + penalizedMap := make(map[string]*entities.Wei) + if u.penalizedRepo == nil || len(quoteHashes) == 0 { + return penalizedMap, nil + } + events, err := u.penalizedRepo.GetPenalizationsByQuoteHashes(ctx, quoteHashes) + if err != nil { + return nil, err + } + for _, ev := range events { + penalizedMap[ev.QuoteHash] = ev.Penalty + } + return penalizedMap, nil +} + +func extractAcceptedPeginHashes(pairs []quote.PeginQuoteWithRetained) []string { + hashes := make([]string, 0, len(pairs)) + for _, pair := range pairs { + if pair.RetainedQuote.QuoteHash != "" { + hashes = append(hashes, pair.RetainedQuote.QuoteHash) + } + } + return hashes +} + +func extractAcceptedPegoutHashes(pairs []quote.PegoutQuoteWithRetained) []string { + hashes := make([]string, 0, len(pairs)) + for _, pair := range pairs { + if pair.RetainedQuote.QuoteHash != "" { + hashes = append(hashes, pair.RetainedQuote.QuoteHash) + } + } + return hashes +} + +func (u *SummariesUseCase) aggregatePeginData(ctx context.Context, startDate, endDate time.Time) (SummaryData, error) { + var ( + quotePairs []quote.PeginQuoteWithRetained + err error + ) + quotePairs, err = u.peginRepo.ListQuotesByDateRange(ctx, startDate, endDate) + if err != nil { + return NewSummaryData(), usecases.WrapUseCaseError(usecases.SummariesUseCaseId, err) + } + data := NewSummaryData() + acceptedQuotesCount := 0 + totals := newSummaryTotals() + penalizedMap, errPen := u.buildPenalizedMap(ctx, extractAcceptedPeginHashes(quotePairs)) + if errPen != nil { + return NewSummaryData(), usecases.WrapUseCaseError(usecases.SummariesUseCaseId, errPen) + } + for _, pair := range quotePairs { + if pair.RetainedQuote.QuoteHash != "" { + acceptedQuotesCount++ + processPeginPair(pair, &data, totals) + if penalty, ok := penalizedMap[pair.RetainedQuote.QuoteHash]; ok { + totals.TotalPenalty.Add(totals.TotalPenalty, penalty) + } + } + } + data.TotalQuotesCount = int64(len(quotePairs)) + data.AcceptedQuotesCount = int64(acceptedQuotesCount) + data.TotalAcceptedQuotedAmount = totals.AcceptedTotalAmount + data.TotalFeesCollected = totals.TotalFees + data.TotalPenaltyAmount = totals.TotalPenalty + lpEarnings := new(entities.Wei) + lpEarnings.Add(lpEarnings, totals.CallFees) + data.LpEarnings = lpEarnings + return data, nil +} + +func (u *SummariesUseCase) aggregatePegoutData(ctx context.Context, startDate, endDate time.Time) (SummaryData, error) { + var ( + quotePairs []quote.PegoutQuoteWithRetained + err error + ) + quotePairs, err = u.pegoutRepo.ListQuotesByDateRange(ctx, startDate, endDate) + if err != nil { + return NewSummaryData(), usecases.WrapUseCaseError(usecases.SummariesUseCaseId, err) + } + data := NewSummaryData() + acceptedQuotesCount := 0 + totals := newSummaryTotals() + penalizedMap, errPen := u.buildPenalizedMap(ctx, extractAcceptedPegoutHashes(quotePairs)) + if errPen != nil { + return NewSummaryData(), usecases.WrapUseCaseError(usecases.SummariesUseCaseId, errPen) + } + for _, pair := range quotePairs { + if pair.RetainedQuote.QuoteHash != "" { + acceptedQuotesCount++ + processPegoutPair(pair, &data, totals) + if penalty, ok := penalizedMap[pair.RetainedQuote.QuoteHash]; ok { + totals.TotalPenalty.Add(totals.TotalPenalty, penalty) + } + } + } + data.TotalQuotesCount = int64(len(quotePairs)) + data.AcceptedQuotesCount = int64(acceptedQuotesCount) + data.TotalAcceptedQuotedAmount = totals.AcceptedTotalAmount + data.TotalFeesCollected = totals.TotalFees + data.TotalPenaltyAmount = totals.TotalPenalty + lpEarnings := new(entities.Wei) + lpEarnings.Add(lpEarnings, totals.CallFees) + data.LpEarnings = lpEarnings + return data, nil +} + +func processPeginPair( + pair quote.PeginQuoteWithRetained, + data *SummaryData, + totals *summaryTotals, +) { + q := pair.Quote + retained := pair.RetainedQuote + totals.AcceptedTotalAmount.Add(totals.AcceptedTotalAmount, q.Value) + callFee, gasFee := q.CallFee, q.GasFee + if isPeginPaidQuote(retained) || isPeginRefundedQuote(retained) { + data.PaidQuotesCount++ + quoteValue := q.Value + data.PaidQuotesAmount.Add(data.PaidQuotesAmount, quoteValue) + totals.CallFees.Add(totals.CallFees, callFee) + totals.TotalFees.Add(totals.TotalFees, callFee) + totals.TotalFees.Add(totals.TotalFees, gasFee) + } + if isPeginRefundedQuote(retained) { + data.RefundedQuotesCount++ + } +} + +func processPegoutPair( + pair quote.PegoutQuoteWithRetained, + data *SummaryData, + totals *summaryTotals, +) { + q := pair.Quote + retained := pair.RetainedQuote + totals.AcceptedTotalAmount.Add(totals.AcceptedTotalAmount, q.Value) + callFee, gasFee := q.CallFee, q.GasFee + if isPegoutPaidQuote(retained) || isPegoutRefundedQuote(retained) { + data.PaidQuotesCount++ + quoteValue := q.Value + data.PaidQuotesAmount.Add(data.PaidQuotesAmount, quoteValue) + totals.CallFees.Add(totals.CallFees, callFee) + totals.TotalFees.Add(totals.TotalFees, callFee) + totals.TotalFees.Add(totals.TotalFees, gasFee) + } + if isPegoutRefundedQuote(retained) { + data.RefundedQuotesCount++ + } +} + +func isPeginPaidQuote(retained quote.RetainedPeginQuote) bool { + return retained.State == quote.PeginStateCallForUserSucceeded +} + +func isPegoutPaidQuote(retained quote.RetainedPegoutQuote) bool { + return retained.State == quote.PegoutStateSendPegoutSucceeded +} + +func isPeginRefundedQuote(retained quote.RetainedPeginQuote) bool { + return retained.State == quote.PeginStateRegisterPegInSucceeded +} + +func isPegoutRefundedQuote(retained quote.RetainedPegoutQuote) bool { + return retained.State == quote.PegoutStateRefundPegOutSucceeded || retained.State == quote.PegoutStateBridgeTxSucceeded +} diff --git a/internal/usecases/reports/summaries_test.go b/internal/usecases/reports/summaries_test.go new file mode 100644 index 00000000..b003d8a4 --- /dev/null +++ b/internal/usecases/reports/summaries_test.go @@ -0,0 +1,296 @@ +package reports_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/rsksmart/liquidity-provider-server/internal/entities" + "github.com/rsksmart/liquidity-provider-server/internal/entities/quote" + "github.com/rsksmart/liquidity-provider-server/internal/usecases/reports" + "github.com/rsksmart/liquidity-provider-server/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestSummariesUseCase_FullSetOfData(t *testing.T) { //nolint:funlen + startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + peginRepo := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepo := mocks.NewPegoutQuoteRepositoryMock(t) + peginQuotes := []quote.PeginQuote{ + { + Value: entities.NewWei(100), + CallFee: entities.NewWei(5), + GasFee: entities.NewWei(2), + PenaltyFee: entities.NewWei(1), + ProductFeeAmount: 3, + }, + { + Value: entities.NewWei(200), + CallFee: entities.NewWei(10), + GasFee: entities.NewWei(4), + PenaltyFee: entities.NewWei(2), + ProductFeeAmount: 6, + }, + } + retainedPeginQuotes := []quote.RetainedPeginQuote{ + { + QuoteHash: "hash1", + Signature: "sig1", + DepositAddress: "addr1", + State: quote.PeginStateCallForUserSucceeded, + UserBtcTxHash: "user_tx1", + CallForUserTxHash: "call_tx1", + }, + { + QuoteHash: "hash2", + Signature: "sig2", + DepositAddress: "addr2", + State: quote.PeginStateCallForUserFailed, + UserBtcTxHash: "user_tx2", + CallForUserTxHash: "", + }, + } + pegoutQuotes := []quote.PegoutQuote{ + { + Value: entities.NewWei(300), + CallFee: entities.NewWei(15), + GasFee: entities.NewWei(6), + PenaltyFee: 10, + ProductFeeAmount: 9, + }, + { + Value: entities.NewWei(400), + CallFee: entities.NewWei(20), + GasFee: entities.NewWei(8), + PenaltyFee: 15, + ProductFeeAmount: 12, + }, + } + retainedPegoutQuotes := []quote.RetainedPegoutQuote{ + { + QuoteHash: "hash3", + Signature: "sig3", + State: quote.PegoutStateSendPegoutSucceeded, + }, + { + QuoteHash: "hash4", + Signature: "sig4", + State: quote.PegoutStateSendPegoutFailed, + }, + } + peginQuotesWithRetained := []quote.PeginQuoteWithRetained{ + { + Quote: peginQuotes[0], + RetainedQuote: retainedPeginQuotes[0], + }, + { + Quote: peginQuotes[1], + RetainedQuote: retainedPeginQuotes[1], + }, + } + pegoutQuotesWithRetained := []quote.PegoutQuoteWithRetained{ + { + Quote: pegoutQuotes[0], + RetainedQuote: retainedPegoutQuotes[0], + }, + { + Quote: pegoutQuotes[1], + RetainedQuote: retainedPegoutQuotes[1], + }, + } + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(peginQuotesWithRetained, nil) + pegoutRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(pegoutQuotesWithRetained, nil) + useCase := reports.NewSummariesUseCase(peginRepo, pegoutRepo, nil) + result, err := useCase.Run(context.Background(), startDate, endDate) + require.NoError(t, err) + assert.Equal(t, int64(len(peginQuotesWithRetained)), result.PeginSummary.TotalQuotesCount) + assert.Equal(t, int64(len(retainedPeginQuotes)), result.PeginSummary.AcceptedQuotesCount) + assert.Equal(t, int64(1), result.PeginSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PeginSummary.PaidQuotesAmount.Cmp(entities.NewWei(100))) + assert.Equal(t, 0, result.PeginSummary.TotalFeesCollected.Cmp(entities.NewWei(7))) + assert.Equal(t, 0, result.PeginSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PeginSummary.LpEarnings.Cmp(entities.NewWei(5))) + assert.Equal(t, int64(len(pegoutQuotesWithRetained)), result.PegoutSummary.TotalQuotesCount) + assert.Equal(t, int64(len(retainedPegoutQuotes)), result.PegoutSummary.AcceptedQuotesCount) + assert.Equal(t, int64(1), result.PegoutSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PegoutSummary.PaidQuotesAmount.Cmp(entities.NewWei(300))) + assert.Equal(t, 0, result.PegoutSummary.TotalFeesCollected.Cmp(entities.NewWei(21))) + assert.Equal(t, 0, result.PegoutSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PegoutSummary.LpEarnings.Cmp(entities.NewWei(15))) + peginRepo.AssertExpectations(t) + pegoutRepo.AssertExpectations(t) +} +func TestSummariesUseCase_OnlyRegularQuotes(t *testing.T) { //nolint:funlen + startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + peginRepo := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepo := mocks.NewPegoutQuoteRepositoryMock(t) + peginQuotes := []quote.PeginQuote{ + { + Value: entities.NewWei(100), + CallFee: entities.NewWei(5), + GasFee: entities.NewWei(2), + PenaltyFee: entities.NewWei(1), + ProductFeeAmount: 3, + }, + { + Value: entities.NewWei(200), + CallFee: entities.NewWei(10), + GasFee: entities.NewWei(4), + PenaltyFee: entities.NewWei(2), + ProductFeeAmount: 6, + }, + } + pegoutQuotes := []quote.PegoutQuote{ + { + Value: entities.NewWei(300), + CallFee: entities.NewWei(15), + GasFee: entities.NewWei(6), + PenaltyFee: 10, + ProductFeeAmount: 9, + }, + } + peginQuotesWithRetained := []quote.PeginQuoteWithRetained{ + { + Quote: peginQuotes[0], + RetainedQuote: quote.RetainedPeginQuote{}, + }, + { + Quote: peginQuotes[1], + RetainedQuote: quote.RetainedPeginQuote{}, + }, + } + pegoutQuotesWithRetained := []quote.PegoutQuoteWithRetained{ + { + Quote: pegoutQuotes[0], + RetainedQuote: quote.RetainedPegoutQuote{}, + }, + } + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(peginQuotesWithRetained, nil) + pegoutRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(pegoutQuotesWithRetained, nil) + useCase := reports.NewSummariesUseCase(peginRepo, pegoutRepo, nil) + result, err := useCase.Run(context.Background(), startDate, endDate) + require.NoError(t, err) + assert.Equal(t, int64(len(peginQuotesWithRetained)), result.PeginSummary.TotalQuotesCount) + assert.Equal(t, int64(0), result.PeginSummary.AcceptedQuotesCount) + assert.Equal(t, int64(0), result.PeginSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PeginSummary.PaidQuotesAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PeginSummary.TotalFeesCollected.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PeginSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PeginSummary.LpEarnings.Cmp(entities.NewWei(0))) + assert.Equal(t, int64(len(pegoutQuotesWithRetained)), result.PegoutSummary.TotalQuotesCount) + assert.Equal(t, int64(0), result.PegoutSummary.AcceptedQuotesCount) + assert.Equal(t, int64(0), result.PegoutSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PegoutSummary.PaidQuotesAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PegoutSummary.TotalFeesCollected.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PegoutSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PegoutSummary.LpEarnings.Cmp(entities.NewWei(0))) + peginRepo.AssertExpectations(t) + pegoutRepo.AssertExpectations(t) +} +func TestSummariesUseCase_OnlyRetainedQuotes(t *testing.T) { //nolint:funlen + startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + peginRepo := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepo := mocks.NewPegoutQuoteRepositoryMock(t) + peginQuote := quote.PeginQuote{ + Value: entities.NewWei(100), + CallFee: entities.NewWei(5), + GasFee: entities.NewWei(2), + PenaltyFee: entities.NewWei(1), + ProductFeeAmount: 3, + } + retainedPeginQuote := quote.RetainedPeginQuote{ + QuoteHash: "hash1", + Signature: "sig1", + DepositAddress: "addr1", + State: quote.PeginStateCallForUserSucceeded, + UserBtcTxHash: "user_tx1", + CallForUserTxHash: "call_tx1", + } + pegoutQuote := quote.PegoutQuote{ + Value: entities.NewWei(300), + CallFee: entities.NewWei(15), + GasFee: entities.NewWei(6), + PenaltyFee: 10, + ProductFeeAmount: 9, + } + retainedPegoutQuote := quote.RetainedPegoutQuote{ + QuoteHash: "hash3", + Signature: "sig3", + State: quote.PegoutStateSendPegoutSucceeded, + } + peginQuotesWithRetained := []quote.PeginQuoteWithRetained{ + { + Quote: peginQuote, + RetainedQuote: retainedPeginQuote, + }, + } + pegoutQuotesWithRetained := []quote.PegoutQuoteWithRetained{ + { + Quote: pegoutQuote, + RetainedQuote: retainedPegoutQuote, + }, + } + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(peginQuotesWithRetained, nil) + pegoutRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return(pegoutQuotesWithRetained, nil) + useCase := reports.NewSummariesUseCase(peginRepo, pegoutRepo, nil) + result, err := useCase.Run(context.Background(), startDate, endDate) + require.NoError(t, err) + assert.Equal(t, int64(len(peginQuotesWithRetained)), result.PeginSummary.TotalQuotesCount) + assert.Equal(t, int64(1), result.PeginSummary.AcceptedQuotesCount) + assert.Equal(t, int64(1), result.PeginSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PeginSummary.PaidQuotesAmount.Cmp(entities.NewWei(100))) + assert.Equal(t, 0, result.PeginSummary.TotalFeesCollected.Cmp(entities.NewWei(7))) + assert.Equal(t, 0, result.PeginSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PeginSummary.LpEarnings.Cmp(entities.NewWei(5))) + assert.Equal(t, int64(len(pegoutQuotesWithRetained)), result.PegoutSummary.TotalQuotesCount) + assert.Equal(t, int64(1), result.PegoutSummary.AcceptedQuotesCount) + assert.Equal(t, int64(1), result.PegoutSummary.PaidQuotesCount) + assert.Equal(t, 0, result.PegoutSummary.PaidQuotesAmount.Cmp(entities.NewWei(300))) + assert.Equal(t, 0, result.PegoutSummary.TotalFeesCollected.Cmp(entities.NewWei(21))) + assert.Equal(t, 0, result.PegoutSummary.TotalPenaltyAmount.Cmp(entities.NewWei(0))) + assert.Equal(t, 0, result.PegoutSummary.LpEarnings.Cmp(entities.NewWei(15))) + peginRepo.AssertExpectations(t) + pegoutRepo.AssertExpectations(t) +} +func TestSummariesUseCase_ErrorGettingPeginQuotes(t *testing.T) { + startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + peginRepo := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepo := mocks.NewPegoutQuoteRepositoryMock(t) + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PeginQuoteWithRetained{}, errors.New("db error")) + useCase := reports.NewSummariesUseCase(peginRepo, pegoutRepo, nil) + _, err := useCase.Run(context.Background(), startDate, endDate) + require.Error(t, err) + assert.Contains(t, err.Error(), "db error") + peginRepo.AssertExpectations(t) + pegoutRepo.AssertExpectations(t) +} +func TestSummariesUseCase_ErrorGettingPegoutQuotes(t *testing.T) { + startDate := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + endDate := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + peginRepo := mocks.NewPeginQuoteRepositoryMock(t) + pegoutRepo := mocks.NewPegoutQuoteRepositoryMock(t) + peginRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PeginQuoteWithRetained{}, nil) + pegoutRepo.On("ListQuotesByDateRange", mock.Anything, startDate, endDate). + Return([]quote.PegoutQuoteWithRetained{}, errors.New("db error")) + useCase := reports.NewSummariesUseCase(peginRepo, pegoutRepo, nil) + _, err := useCase.Run(context.Background(), startDate, endDate) + require.Error(t, err) + assert.Contains(t, err.Error(), "db error") + peginRepo.AssertExpectations(t) + pegoutRepo.AssertExpectations(t) +} diff --git a/pkg/liquidity_provider.go b/pkg/liquidity_provider.go index c7e41517..f8adbeca 100644 --- a/pkg/liquidity_provider.go +++ b/pkg/liquidity_provider.go @@ -8,6 +8,7 @@ import ( "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider" "github.com/rsksmart/liquidity-provider-server/internal/entities/utils" + reports "github.com/rsksmart/liquidity-provider-server/internal/usecases/reports" ) type ProviderDetail struct { @@ -162,6 +163,44 @@ type ServerInfoDTO struct { Revision string `json:"revision" example:"b7bf393a2b1cedde8ee15b00780f44e6e5d2ba9d" description:"Version commit hash" required:""` } +type SummaryDataDTO struct { + TotalQuotesCount int64 `json:"totalQuotesCount"` + AcceptedQuotesCount int64 `json:"acceptedQuotesCount"` + PaidQuotesCount int64 `json:"paidQuotesCount"` + PaidQuotesAmount *big.Int `json:"paidQuotesAmount"` + TotalAcceptedQuotedAmount *big.Int `json:"totalAcceptedQuotedAmount"` + TotalFeesCollected *big.Int `json:"totalFeesCollected"` + RefundedQuotesCount int64 `json:"refundedQuotesCount"` + TotalPenaltyAmount *big.Int `json:"totalPenaltyAmount"` + LpEarnings *big.Int `json:"lpEarnings"` +} + +type SummaryResultDTO struct { + PeginSummary SummaryDataDTO `json:"peginSummary"` + PegoutSummary SummaryDataDTO `json:"pegoutSummary"` +} + +func ToSummaryDataDTO(data reports.SummaryData) SummaryDataDTO { + return SummaryDataDTO{ + TotalQuotesCount: data.TotalQuotesCount, + AcceptedQuotesCount: data.AcceptedQuotesCount, + PaidQuotesCount: data.PaidQuotesCount, + PaidQuotesAmount: data.PaidQuotesAmount.AsBigInt(), + TotalAcceptedQuotedAmount: data.TotalAcceptedQuotedAmount.AsBigInt(), + TotalFeesCollected: data.TotalFeesCollected.AsBigInt(), + RefundedQuotesCount: data.RefundedQuotesCount, + TotalPenaltyAmount: data.TotalPenaltyAmount.AsBigInt(), + LpEarnings: data.LpEarnings.AsBigInt(), + } +} + +func ToSummaryResultDTO(result reports.SummaryResult) SummaryResultDTO { + return SummaryResultDTO{ + PeginSummary: ToSummaryDataDTO(result.PeginSummary), + PegoutSummary: ToSummaryDataDTO(result.PegoutSummary), + } +} + func ToAvailableLiquidityDTO(entity liquidity_provider.AvailableLiquidity) AvailableLiquidityDTO { return AvailableLiquidityDTO{ PeginLiquidityAmount: entity.PeginLiquidity.AsBigInt(), diff --git a/pkg/liquidity_provider_test.go b/pkg/liquidity_provider_test.go index a0146fed..eec56304 100644 --- a/pkg/liquidity_provider_test.go +++ b/pkg/liquidity_provider_test.go @@ -1,14 +1,16 @@ package pkg_test import ( - "github.com/stretchr/testify/require" "math/big" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/rsksmart/liquidity-provider-server/internal/entities" "github.com/rsksmart/liquidity-provider-server/internal/entities/liquidity_provider" "github.com/rsksmart/liquidity-provider-server/internal/entities/utils" + "github.com/rsksmart/liquidity-provider-server/internal/usecases/reports" "github.com/rsksmart/liquidity-provider-server/pkg" "github.com/rsksmart/liquidity-provider-server/test" "github.com/stretchr/testify/assert" @@ -291,3 +293,62 @@ func TestGetReportsByPeriodRequest_GetTimestamps(t *testing.T) { assert.Equal(t, 20, endTime.Day()) }) } + +func TestToSummaryDataDTO(t *testing.T) { + data := reports.SummaryData{ + TotalQuotesCount: 10, + AcceptedQuotesCount: 8, + PaidQuotesCount: 5, + PaidQuotesAmount: entities.NewWei(1500), + TotalAcceptedQuotedAmount: entities.NewWei(2500), + TotalFeesCollected: entities.NewWei(300), + RefundedQuotesCount: 2, + TotalPenaltyAmount: entities.NewWei(50), + LpEarnings: entities.NewWei(250), + } + dto := pkg.ToSummaryDataDTO(data) + assert.Equal(t, data.TotalQuotesCount, dto.TotalQuotesCount) + assert.Equal(t, data.AcceptedQuotesCount, dto.AcceptedQuotesCount) + assert.Equal(t, data.PaidQuotesCount, dto.PaidQuotesCount) + assert.Equal(t, 0, data.PaidQuotesAmount.AsBigInt().Cmp(dto.PaidQuotesAmount)) + assert.Equal(t, 0, data.TotalAcceptedQuotedAmount.AsBigInt().Cmp(dto.TotalAcceptedQuotedAmount)) + assert.Equal(t, 0, data.TotalFeesCollected.AsBigInt().Cmp(dto.TotalFeesCollected)) + assert.Equal(t, data.RefundedQuotesCount, dto.RefundedQuotesCount) + assert.Equal(t, 0, data.TotalPenaltyAmount.AsBigInt().Cmp(dto.TotalPenaltyAmount)) + assert.Equal(t, 0, data.LpEarnings.AsBigInt().Cmp(dto.LpEarnings)) + test.AssertNonZeroValues(t, dto) +} + +func TestToSummaryResultDTO(t *testing.T) { + pegin := reports.SummaryData{ + TotalQuotesCount: 3, + AcceptedQuotesCount: 2, + PaidQuotesCount: 1, + PaidQuotesAmount: entities.NewWei(500), + TotalAcceptedQuotedAmount: entities.NewWei(800), + TotalFeesCollected: entities.NewWei(100), + RefundedQuotesCount: 0, + TotalPenaltyAmount: entities.NewWei(0), + LpEarnings: entities.NewWei(100), + } + pegout := reports.SummaryData{ + TotalQuotesCount: 4, + AcceptedQuotesCount: 3, + PaidQuotesCount: 2, + PaidQuotesAmount: entities.NewWei(900), + TotalAcceptedQuotedAmount: entities.NewWei(1200), + TotalFeesCollected: entities.NewWei(150), + RefundedQuotesCount: 1, + TotalPenaltyAmount: entities.NewWei(20), + LpEarnings: entities.NewWei(130), + } + result := reports.SummaryResult{PeginSummary: pegin, PegoutSummary: pegout} + dto := pkg.ToSummaryResultDTO(result) + expected := pkg.SummaryResultDTO{ + PeginSummary: pkg.ToSummaryDataDTO(pegin), + PegoutSummary: pkg.ToSummaryDataDTO(pegout), + } + assert.Equal(t, expected, dto) + test.AssertMaxZeroValues(t, dto.PeginSummary, 1) + test.AssertNonZeroValues(t, dto.PegoutSummary) +} \ No newline at end of file diff --git a/test/mocks/pegin_quote_repository_mock.go b/test/mocks/pegin_quote_repository_mock.go index 24301308..cc1d73e3 100644 --- a/test/mocks/pegin_quote_repository_mock.go +++ b/test/mocks/pegin_quote_repository_mock.go @@ -474,6 +474,66 @@ func (_c *PeginQuoteRepositoryMock_InsertRetainedQuote_Call) RunAndReturn(run fu return _c } +// ListQuotesByDateRange provides a mock function with given fields: ctx, startDate, endDate +func (_m *PeginQuoteRepositoryMock) ListQuotesByDateRange(ctx context.Context, startDate time.Time, endDate time.Time) ([]quote.PeginQuoteWithRetained, error) { + ret := _m.Called(ctx, startDate, endDate) + + if len(ret) == 0 { + panic("no return value specified for ListQuotesByDateRange") + } + + var r0 []quote.PeginQuoteWithRetained + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time, time.Time) ([]quote.PeginQuoteWithRetained, error)); ok { + return rf(ctx, startDate, endDate) + } + if rf, ok := ret.Get(0).(func(context.Context, time.Time, time.Time) []quote.PeginQuoteWithRetained); ok { + r0 = rf(ctx, startDate, endDate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]quote.PeginQuoteWithRetained) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, time.Time, time.Time) error); ok { + r1 = rf(ctx, startDate, endDate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PeginQuoteRepositoryMock_ListQuotesByDateRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListQuotesByDateRange' +type PeginQuoteRepositoryMock_ListQuotesByDateRange_Call struct { + *mock.Call +} + +// ListQuotesByDateRange is a helper method to define mock.On call +// - ctx context.Context +// - startDate time.Time +// - endDate time.Time +func (_e *PeginQuoteRepositoryMock_Expecter) ListQuotesByDateRange(ctx interface{}, startDate interface{}, endDate interface{}) *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call { + return &PeginQuoteRepositoryMock_ListQuotesByDateRange_Call{Call: _e.mock.On("ListQuotesByDateRange", ctx, startDate, endDate)} +} + +func (_c *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call) Run(run func(ctx context.Context, startDate time.Time, endDate time.Time)) *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(time.Time), args[2].(time.Time)) + }) + return _c +} + +func (_c *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call) Return(_a0 []quote.PeginQuoteWithRetained, _a1 error) *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call) RunAndReturn(run func(context.Context, time.Time, time.Time) ([]quote.PeginQuoteWithRetained, error)) *PeginQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Return(run) + return _c +} + // UpdateRetainedQuote provides a mock function with given fields: ctx, _a1 func (_m *PeginQuoteRepositoryMock) UpdateRetainedQuote(ctx context.Context, _a1 quote.RetainedPeginQuote) error { ret := _m.Called(ctx, _a1) diff --git a/test/mocks/pegout_quote_repository_mock.go b/test/mocks/pegout_quote_repository_mock.go index 43a315ef..93646896 100644 --- a/test/mocks/pegout_quote_repository_mock.go +++ b/test/mocks/pegout_quote_repository_mock.go @@ -1,5 +1,6 @@ // Code generated by mockery v2.53.0. DO NOT EDIT. + package mocks import ( @@ -533,6 +534,66 @@ func (_c *PegoutQuoteRepositoryMock_ListPegoutDepositsByAddress_Call) RunAndRetu return _c } +// ListQuotesByDateRange provides a mock function with given fields: ctx, startDate, endDate +func (_m *PegoutQuoteRepositoryMock) ListQuotesByDateRange(ctx context.Context, startDate time.Time, endDate time.Time) ([]quote.PegoutQuoteWithRetained, error) { + ret := _m.Called(ctx, startDate, endDate) + + if len(ret) == 0 { + panic("no return value specified for ListQuotesByDateRange") + } + + var r0 []quote.PegoutQuoteWithRetained + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, time.Time, time.Time) ([]quote.PegoutQuoteWithRetained, error)); ok { + return rf(ctx, startDate, endDate) + } + if rf, ok := ret.Get(0).(func(context.Context, time.Time, time.Time) []quote.PegoutQuoteWithRetained); ok { + r0 = rf(ctx, startDate, endDate) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]quote.PegoutQuoteWithRetained) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, time.Time, time.Time) error); ok { + r1 = rf(ctx, startDate, endDate) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListQuotesByDateRange' +type PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call struct { + *mock.Call +} + +// ListQuotesByDateRange is a helper method to define mock.On call +// - ctx context.Context +// - startDate time.Time +// - endDate time.Time +func (_e *PegoutQuoteRepositoryMock_Expecter) ListQuotesByDateRange(ctx interface{}, startDate interface{}, endDate interface{}) *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call { + return &PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call{Call: _e.mock.On("ListQuotesByDateRange", ctx, startDate, endDate)} +} + +func (_c *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call) Run(run func(ctx context.Context, startDate time.Time, endDate time.Time)) *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(time.Time), args[2].(time.Time)) + }) + return _c +} + +func (_c *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call) Return(_a0 []quote.PegoutQuoteWithRetained, _a1 error) *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call) RunAndReturn(run func(context.Context, time.Time, time.Time) ([]quote.PegoutQuoteWithRetained, error)) *PegoutQuoteRepositoryMock_ListQuotesByDateRange_Call { + _c.Call.Return(run) + return _c +} + // UpdateRetainedQuote provides a mock function with given fields: ctx, _a1 func (_m *PegoutQuoteRepositoryMock) UpdateRetainedQuote(ctx context.Context, _a1 quote.RetainedPegoutQuote) error { ret := _m.Called(ctx, _a1) diff --git a/test/mocks/use_case_registry_mock.go b/test/mocks/use_case_registry_mock.go index c649d117..9515e1b8 100644 --- a/test/mocks/use_case_registry_mock.go +++ b/test/mocks/use_case_registry_mock.go @@ -1391,6 +1391,53 @@ func (_c *UseCaseRegistryMock_SetPegoutConfigUseCase_Call) RunAndReturn(run func return _c } +// SummariesUseCase provides a mock function with no fields +func (_m *UseCaseRegistryMock) SummariesUseCase() *reports.SummariesUseCase { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SummariesUseCase") + } + + var r0 *reports.SummariesUseCase + if rf, ok := ret.Get(0).(func() *reports.SummariesUseCase); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*reports.SummariesUseCase) + } + } + + return r0 +} + +// UseCaseRegistryMock_SummariesUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SummariesUseCase' +type UseCaseRegistryMock_SummariesUseCase_Call struct { + *mock.Call +} + +// SummariesUseCase is a helper method to define mock.On call +func (_e *UseCaseRegistryMock_Expecter) SummariesUseCase() *UseCaseRegistryMock_SummariesUseCase_Call { + return &UseCaseRegistryMock_SummariesUseCase_Call{Call: _e.mock.On("SummariesUseCase")} +} + +func (_c *UseCaseRegistryMock_SummariesUseCase_Call) Run(run func()) *UseCaseRegistryMock_SummariesUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseRegistryMock_SummariesUseCase_Call) Return(_a0 *reports.SummariesUseCase) *UseCaseRegistryMock_SummariesUseCase_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseRegistryMock_SummariesUseCase_Call) RunAndReturn(run func() *reports.SummariesUseCase) *UseCaseRegistryMock_SummariesUseCase_Call { + _c.Call.Return(run) + return _c +} + // WithdrawCollateralUseCase provides a mock function with no fields func (_m *UseCaseRegistryMock) WithdrawCollateralUseCase() *liquidity_provider.WithdrawCollateralUseCase { ret := _m.Called()