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
+
+
+
+
+
+
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()