Skip to content

Commit 1b69591

Browse files
DominikkqAndresQuijano
authored andcommitted
feat:Financial API + ManagementUI [GBI-2537] (#711)
1 parent 6232ad0 commit 1b69591

File tree

25 files changed

+1534
-15
lines changed

25 files changed

+1534
-15
lines changed

OpenApi.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,38 @@ components:
684684
rsk:
685685
type: string
686686
type: object
687+
SummaryData:
688+
properties:
689+
acceptedQuotesCount:
690+
type: integer
691+
lpEarnings:
692+
$ref: '#/components/schemas/Wei'
693+
paidQuotesAmount:
694+
$ref: '#/components/schemas/Wei'
695+
paidQuotesCount:
696+
type: integer
697+
refundedQuotesCount:
698+
type: integer
699+
totalAcceptedQuotedAmount:
700+
$ref: '#/components/schemas/Wei'
701+
totalFeesCollected:
702+
$ref: '#/components/schemas/Wei'
703+
totalPenaltyAmount:
704+
$ref: '#/components/schemas/Wei'
705+
totalQuotesCount:
706+
type: integer
707+
type: object
708+
SummaryResult:
709+
properties:
710+
peginSummary:
711+
$ref: '#/components/schemas/SummaryData'
712+
type: object
713+
pegoutSummary:
714+
$ref: '#/components/schemas/SummaryData'
715+
type: object
716+
type: object
717+
Wei: {}
718+
entities.Wei: {}
687719
pkg.AcceptQuoteRequest:
688720
properties:
689721
quoteHash:
@@ -1041,6 +1073,34 @@ paths:
10411073
"200":
10421074
description: ""
10431075
summary: Get asset Reports
1076+
/report/summaries:
1077+
get:
1078+
description: ' Returns financial data for a given period'
1079+
parameters:
1080+
- description: Start date in YYYY-MM-DD format
1081+
in: query
1082+
name: startDate
1083+
required: true
1084+
schema:
1085+
description: Start date in YYYY-MM-DD format
1086+
format: string
1087+
type: string
1088+
- description: End date in YYYY-MM-DD format
1089+
in: query
1090+
name: endDate
1091+
required: true
1092+
schema:
1093+
description: End date in YYYY-MM-DD format
1094+
format: string
1095+
type: string
1096+
responses:
1097+
"200":
1098+
content:
1099+
application/json:
1100+
schema:
1101+
$ref: '#/components/schemas/SummaryResult'
1102+
description: Financial data for the given period
1103+
summary: Summaries
10441104
/reports/pegin:
10451105
get:
10461106
description: ' Get the last pegins on the API. Included in the management API.'

internal/adapters/dataproviders/database/mongo/pegin.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"time"
8+
79
"github.com/rsksmart/liquidity-provider-server/internal/entities/quote"
810
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
911
log "github.com/sirupsen/logrus"
1012
"go.mongodb.org/mongo-driver/bson"
1113
"go.mongodb.org/mongo-driver/bson/primitive"
1214
"go.mongodb.org/mongo-driver/mongo"
13-
"time"
15+
"go.mongodb.org/mongo-driver/mongo/options"
1416
)
1517

1618
const (
@@ -251,3 +253,55 @@ func (repo *peginMongoRepository) DeleteQuotes(ctx context.Context, quotes []str
251253
}
252254
return uint(peginResult.DeletedCount + retainedResult.DeletedCount + creationDataResult.DeletedCount), nil
253255
}
256+
257+
func (repo *peginMongoRepository) ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]quote.PeginQuoteWithRetained, error) {
258+
result := make([]quote.PeginQuoteWithRetained, 0)
259+
dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout)
260+
defer cancel()
261+
quoteFilter := bson.D{{Key: "agreement_timestamp", Value: bson.D{
262+
{Key: "$gte", Value: startDate.Unix()},
263+
{Key: "$lte", Value: endDate.Unix()},
264+
}}}
265+
findOpts := options.Find().SetSort(bson.D{{Key: "agreement_timestamp", Value: 1}})
266+
quoteCursor, err := repo.conn.Collection(PeginQuoteCollection).Find(dbCtx, quoteFilter, findOpts)
267+
if err != nil {
268+
return nil, err
269+
}
270+
var storedQuotes []StoredPeginQuote
271+
if err = quoteCursor.All(dbCtx, &storedQuotes); err != nil {
272+
return nil, err
273+
}
274+
if len(storedQuotes) == 0 {
275+
logDbInteraction(Read, result)
276+
return result, nil
277+
}
278+
hashToIndex := make(map[string]int, len(storedQuotes))
279+
quoteHashes := make([]string, len(storedQuotes))
280+
result = make([]quote.PeginQuoteWithRetained, len(storedQuotes))
281+
for i, stored := range storedQuotes {
282+
quoteHashes[i] = stored.Hash
283+
hashToIndex[stored.Hash] = i
284+
result[i] = quote.PeginQuoteWithRetained{
285+
Quote: stored.PeginQuote,
286+
RetainedQuote: quote.RetainedPeginQuote{},
287+
}
288+
}
289+
retainedCursor, err := repo.conn.Collection(RetainedPeginQuoteCollection).Find(
290+
dbCtx,
291+
bson.D{{Key: "quote_hash", Value: bson.D{{Key: "$in", Value: quoteHashes}}}},
292+
)
293+
if err != nil {
294+
return result, err
295+
}
296+
var retainedQuotes []quote.RetainedPeginQuote
297+
if err = retainedCursor.All(dbCtx, &retainedQuotes); err != nil {
298+
return result, err
299+
}
300+
for _, retainedQuote := range retainedQuotes {
301+
if idx, exists := hashToIndex[retainedQuote.QuoteHash]; exists {
302+
result[idx].RetainedQuote = retainedQuote
303+
}
304+
}
305+
logDbInteraction(Read, len(result))
306+
return result, nil
307+
}

internal/adapters/dataproviders/database/mongo/pegout.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"regexp"
8+
"time"
9+
710
"github.com/rsksmart/liquidity-provider-server/internal/entities/quote"
811
"github.com/rsksmart/liquidity-provider-server/internal/usecases"
912
log "github.com/sirupsen/logrus"
1013
"go.mongodb.org/mongo-driver/bson"
1114
"go.mongodb.org/mongo-driver/bson/primitive"
1215
"go.mongodb.org/mongo-driver/mongo"
1316
"go.mongodb.org/mongo-driver/mongo/options"
14-
"regexp"
15-
"time"
1617
)
1718

1819
const (
@@ -360,3 +361,55 @@ func (repo *pegoutMongoRepository) UpsertPegoutDeposits(ctx context.Context, dep
360361
}
361362
return err
362363
}
364+
365+
func (repo *pegoutMongoRepository) ListQuotesByDateRange(ctx context.Context, startDate, endDate time.Time) ([]quote.PegoutQuoteWithRetained, error) {
366+
result := make([]quote.PegoutQuoteWithRetained, 0)
367+
dbCtx, cancel := context.WithTimeout(ctx, repo.conn.timeout)
368+
defer cancel()
369+
quoteFilter := bson.D{{Key: "agreement_timestamp", Value: bson.D{
370+
{Key: "$gte", Value: startDate.Unix()},
371+
{Key: "$lte", Value: endDate.Unix()},
372+
}}}
373+
findOpts := options.Find().SetSort(bson.D{{Key: "agreement_timestamp", Value: 1}})
374+
quoteCursor, err := repo.conn.Collection(PegoutQuoteCollection).Find(dbCtx, quoteFilter, findOpts)
375+
if err != nil {
376+
return nil, err
377+
}
378+
var storedQuotes []StoredPegoutQuote
379+
if err = quoteCursor.All(dbCtx, &storedQuotes); err != nil {
380+
return nil, err
381+
}
382+
if len(storedQuotes) == 0 {
383+
logDbInteraction(Read, result)
384+
return result, nil
385+
}
386+
hashToIndex := make(map[string]int, len(storedQuotes))
387+
quoteHashes := make([]string, len(storedQuotes))
388+
result = make([]quote.PegoutQuoteWithRetained, len(storedQuotes))
389+
for i, stored := range storedQuotes {
390+
quoteHashes[i] = stored.Hash
391+
hashToIndex[stored.Hash] = i
392+
result[i] = quote.PegoutQuoteWithRetained{
393+
Quote: stored.PegoutQuote,
394+
RetainedQuote: quote.RetainedPegoutQuote{},
395+
}
396+
}
397+
retainedCursor, err := repo.conn.Collection(RetainedPegoutQuoteCollection).Find(
398+
dbCtx,
399+
bson.D{{Key: "quote_hash", Value: bson.D{{Key: "$in", Value: quoteHashes}}}},
400+
)
401+
if err != nil {
402+
return result, err
403+
}
404+
var retainedQuotes []quote.RetainedPegoutQuote
405+
if err = retainedCursor.All(dbCtx, &retainedQuotes); err != nil {
406+
return result, err
407+
}
408+
for _, retainedQuote := range retainedQuotes {
409+
if idx, exists := hashToIndex[retainedQuote.QuoteHash]; exists {
410+
result[idx].RetainedQuote = retainedQuote
411+
}
412+
}
413+
logDbInteraction(Read, len(result))
414+
return result, nil
415+
}

internal/adapters/entrypoints/rest/assets/management.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,51 @@ <h5 class="card-title">Current Configuration</h5>
118118
</div>
119119
</div>
120120
</div>
121+
<div class="row mt-3">
122+
<div class="col-md-12">
123+
<div class="card">
124+
<div class="card-header">Reports</div>
125+
<div class="card-body">
126+
<h5 class="card-title">Financial Summaries</h5>
127+
<div class="row">
128+
<div class="col-md-5">
129+
<div class="mb-3">
130+
<label for="summaryStartDate" class="form-label">Start Date</label>
131+
<input type="date" class="form-control" id="summaryStartDate">
132+
</div>
133+
</div>
134+
<div class="col-md-5">
135+
<div class="mb-3">
136+
<label for="summaryEndDate" class="form-label">End Date</label>
137+
<input type="date" class="form-control" id="summaryEndDate">
138+
</div>
139+
</div>
140+
<div class="col-md-2 d-flex align-items-end">
141+
<div class="mb-3">
142+
<button type="button" class="btn btn-primary" id="fetchSummariesButton">Generate Report</button>
143+
</div>
144+
</div>
145+
</div>
146+
<div id="summariesResult" style="display: none;">
147+
<div class="row mt-3">
148+
<div class="col-md-6">
149+
<div class="card">
150+
<div class="card-header">Pegin Summary</div>
151+
<div class="card-body" id="peginSummary"></div>
152+
</div>
153+
</div>
154+
<div class="col-md-6">
155+
<div class="card">
156+
<div class="card-header">Pegout Summary</div>
157+
<div class="card-body" id="pegoutSummary"></div>
158+
</div>
159+
</div>
160+
</div>
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
121166
</div>
122167

123168
<div class="toast-container">

internal/adapters/entrypoints/rest/assets/static/management.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,17 @@ pre {
4646
50% { width: 50%; }
4747
100% { width: 100%; }
4848
}
49+
50+
#summariesResult {
51+
margin-top: 20px;
52+
}
53+
54+
#peginSummary table,
55+
#pegoutSummary table {
56+
margin-bottom: 0;
57+
}
58+
59+
#peginSummary th,
60+
#pegoutSummary th {
61+
width: 60%;
62+
}

internal/adapters/entrypoints/rest/assets/static/management.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,62 @@ const addCollateral = async (amountId, endpoint, elementId, loadingBarId, button
613613
}
614614
};
615615

616+
const displaySummaryData = (container, data) => {
617+
container.innerHTML = '';
618+
const table = document.createElement('table');
619+
table.classList.add('table', 'table-striped');
620+
const rows = [
621+
{ label: 'Total Quotes', value: data.totalQuotesCount },
622+
{ label: 'Accepted Quotes', value: data.acceptedQuotesCount },
623+
{ label: 'Paid Quotes', value: data.paidQuotesCount },
624+
{ label: 'Paid Quotes Amount', value: data.paidQuotesAmount },
625+
{ label: 'Total Accepted Amount', value: data.totalAcceptedQuotedAmount },
626+
{ label: 'Total Fees Collected', value: data.totalFeesCollected },
627+
{ label: 'Refunded Quotes', value: data.refundedQuotesCount },
628+
{ label: 'Total Penalty Amount', value: data.totalPenaltyAmount },
629+
{ label: 'LP Earnings', value: data.lpEarnings }
630+
];
631+
rows.forEach(row => {
632+
const tr = document.createElement('tr');
633+
const th = document.createElement('th');
634+
th.textContent = row.label;
635+
const td = document.createElement('td');
636+
td.textContent = row.value;
637+
tr.appendChild(th);
638+
tr.appendChild(td);
639+
table.appendChild(tr);
640+
});
641+
container.appendChild(table);
642+
};
643+
644+
const fetchSummariesReport = async (csrfToken) => {
645+
const startDate = document.getElementById('summaryStartDate').value;
646+
const endDate = document.getElementById('summaryEndDate').value;
647+
if (!startDate || !endDate) {
648+
showErrorToast('Please select both start and end dates');
649+
return;
650+
}
651+
try {
652+
const response = await fetch(`/report/summaries?startDate=${startDate}&endDate=${endDate}`, {
653+
method: 'GET',
654+
headers: {
655+
'Content-Type': 'application/json',
656+
'X-CSRF-Token': csrfToken
657+
}
658+
});
659+
if (!response.ok) {
660+
const errorData = await response.json();
661+
throw new Error(errorData.message || 'Failed to fetch summaries');
662+
}
663+
const data = await response.json();
664+
document.getElementById('summariesResult').style.display = 'block';
665+
displaySummaryData(document.getElementById('peginSummary'), data.peginSummary);
666+
displaySummaryData(document.getElementById('pegoutSummary'), data.pegoutSummary);
667+
} catch (error) {
668+
showErrorToast(`Error fetching summaries: ${error.message}`);
669+
}
670+
};
671+
616672
document.addEventListener('DOMContentLoaded', () => {
617673
const csrfToken = data.CsrfToken;
618674
const configurations = data.Configuration;
@@ -623,6 +679,7 @@ document.addEventListener('DOMContentLoaded', () => {
623679
document.getElementById('addPeginCollateralButton').addEventListener('click', () => addCollateral('addPeginCollateralAmount', '/pegin/addCollateral', 'peginCollateral', 'peginLoadingBar', 'addPeginCollateralButton', csrfToken));
624680
document.getElementById('addPegoutCollateralButton').addEventListener('click', () => addCollateral('addPegoutCollateralAmount', '/pegout/addCollateral', 'pegoutCollateral', 'pegoutLoadingBar', 'addPegoutCollateralButton', csrfToken));
625681
document.getElementById('saveConfig').addEventListener('click', () => saveConfig(csrfToken, configurations));
682+
document.getElementById('fetchSummariesButton').addEventListener('click', () => fetchSummariesReport(csrfToken));
626683

627684
document.querySelectorAll('#configTabs a[data-bs-toggle="tab"]').forEach(tabEl => {
628685
tabEl.addEventListener('shown.bs.tab', () => checkFeeWarnings());
@@ -636,4 +693,11 @@ document.addEventListener('DOMContentLoaded', () => {
636693
fetchData('/pegin/collateral', 'peginCollateral', csrfToken);
637694
fetchData('/pegout/collateral', 'pegoutCollateral', csrfToken);
638695
checkFeeWarnings();
696+
697+
const today = new Date();
698+
const lastMonth = new Date(today);
699+
lastMonth.setMonth(today.getMonth() - 1);
700+
701+
document.getElementById('summaryStartDate').value = lastMonth.toISOString().split('T')[0];
702+
document.getElementById('summaryEndDate').value = today.toISOString().split('T')[0];
639703
});

0 commit comments

Comments
 (0)