Skip to content

Commit bc2da9e

Browse files
xdefragclaude
andcommitted
fix: address PR review findings for MTLAX synthetic category
- Fix pagination links in home.html to preserve synthetic_offset across sections - Add MTLAXBalance field to SyntheticRow and SearchAccountRow for consistency - Surface MTLAX membership in search results with IsSynthetic flag and [MTLAX] badge - Add GetSynthetic error test for 500 response - Add getMTLAXBalance unit tests (nil/zero/non-zero trustline cases) - Replace mock.Anything with exact offset values in handler tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c3a6148 commit bc2da9e

6 files changed

Lines changed: 78 additions & 11 deletions

File tree

internal/handler/handler_test.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestHomeHandler(t *testing.T) {
7878
}, nil)
7979

8080
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, mock.Anything).Return([]repository.SyntheticRow{
81-
{AccountID: "GHIJ", Name: "Test Synthetic", ReputationScore: 3.5, ReputationWeight: 10.0},
81+
{AccountID: "GHIJ", Name: "Test Synthetic", MTLAXBalance: 1.0, ReputationScore: 3.5, ReputationWeight: 10.0},
8282
}, nil)
8383

8484
var renderedData any
@@ -109,10 +109,10 @@ func TestHomeHandler(t *testing.T) {
109109
tmpl := mocks.NewMockTemplateRenderer(t)
110110

111111
accounts.EXPECT().GetStats(mock.Anything).Return(&repository.Stats{}, nil)
112-
// Expect offset 20 for persons and 40 for corporate
112+
// Expect offset 20 for persons, 40 for corporate, 0 for synthetic
113113
accounts.EXPECT().GetPersons(mock.Anything, mock.Anything, 20).Return(nil, nil)
114114
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, 40).Return(nil, nil)
115-
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
115+
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, 0).Return(nil, nil)
116116
tmpl.EXPECT().Render(mock.Anything, mock.Anything, mock.Anything).Return(nil)
117117

118118
h, err := New(stellar, accounts, nil, tmpl)
@@ -133,8 +133,8 @@ func TestHomeHandler(t *testing.T) {
133133

134134
accounts.EXPECT().GetStats(mock.Anything).Return(&repository.Stats{}, nil)
135135
accounts.EXPECT().GetPersons(mock.Anything, mock.Anything, 0).Return(nil, nil)
136-
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
137-
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
136+
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, 0).Return(nil, nil)
137+
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, 0).Return(nil, nil)
138138
tmpl.EXPECT().Render(mock.Anything, mock.Anything, mock.Anything).Return(nil)
139139

140140
h, err := New(stellar, accounts, nil, tmpl)
@@ -155,8 +155,8 @@ func TestHomeHandler(t *testing.T) {
155155

156156
accounts.EXPECT().GetStats(mock.Anything).Return(&repository.Stats{}, nil)
157157
accounts.EXPECT().GetPersons(mock.Anything, mock.Anything, 0).Return(nil, nil)
158-
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
159-
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
158+
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, 0).Return(nil, nil)
159+
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, 0).Return(nil, nil)
160160
tmpl.EXPECT().Render(mock.Anything, mock.Anything, mock.Anything).Return(nil)
161161

162162
h, err := New(stellar, accounts, nil, tmpl)
@@ -230,6 +230,28 @@ func TestHomeHandler(t *testing.T) {
230230
assert.Contains(t, w.Body.String(), "Failed to fetch corporate")
231231
})
232232

233+
t.Run("synthetic error returns 500", func(t *testing.T) {
234+
accounts := mocks.NewMockAccountQuerier(t)
235+
stellar := mocks.NewMockStellarServicer(t)
236+
tmpl := mocks.NewMockTemplateRenderer(t)
237+
238+
accounts.EXPECT().GetStats(mock.Anything).Return(&repository.Stats{}, nil)
239+
accounts.EXPECT().GetPersons(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
240+
accounts.EXPECT().GetCorporate(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
241+
accounts.EXPECT().GetSynthetic(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("database error"))
242+
243+
h, err := New(stellar, accounts, nil, tmpl)
244+
require.NoError(t, err)
245+
246+
req := httptest.NewRequest(http.MethodGet, "/", nil)
247+
w := httptest.NewRecorder()
248+
249+
h.Home(w, req)
250+
251+
assert.Equal(t, http.StatusInternalServerError, w.Code)
252+
assert.Contains(t, w.Body.String(), "Failed to fetch synthetic")
253+
})
254+
233255
t.Run("template render error returns 500", func(t *testing.T) {
234256
accounts := mocks.NewMockAccountQuerier(t)
235257
stellar := mocks.NewMockStellarServicer(t)

internal/handler/search.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ type SearchAccountDisplay struct {
6060
Name string
6161
MTLAPBalance float64
6262
MTLACBalance float64
63+
MTLAXBalance float64
6364
TotalXLMValue float64
6465
IsPerson bool
6566
IsCorporate bool
67+
IsSynthetic bool
6668
ReputationScore float64 // Weighted reputation score
6769
ReputationGrade string // "A", "A-", "B+", etc.
6870
ReputationWeight float64 // Total weight of raters
@@ -151,9 +153,11 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
151153
Name: row.Name,
152154
MTLAPBalance: row.MTLAPBalance,
153155
MTLACBalance: row.MTLACBalance,
156+
MTLAXBalance: row.MTLAXBalance,
154157
TotalXLMValue: row.TotalXLMValue,
155158
IsPerson: row.MTLAPBalance > 0 && row.MTLAPBalance <= 5,
156159
IsCorporate: row.MTLACBalance > 0 && row.MTLACBalance <= 4,
160+
IsSynthetic: row.MTLAXBalance > 0,
157161
ReputationScore: row.ReputationScore,
158162
ReputationGrade: grade,
159163
ReputationWeight: row.ReputationWeight,

internal/repository/account.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type CorporateRow struct {
5757
type SyntheticRow struct {
5858
AccountID string
5959
Name string
60+
MTLAXBalance float64
6061
ReputationScore float64
6162
ReputationWeight float64
6263
}
@@ -181,6 +182,7 @@ func (r *AccountRepository) GetSynthetic(ctx context.Context, limit int, offset
181182
Select(
182183
"a.account_id",
183184
"COALESCE(m.data_value, CONCAT(LEFT(a.account_id, 6), '...', RIGHT(a.account_id, 6))) AS name",
185+
"COALESCE(a.mtlax_balance, 0) AS mtlax_balance",
184186
"COALESCE(rs.weighted_score, 0) AS reputation_score",
185187
"COALESCE(rs.total_weight, 0) AS reputation_weight",
186188
).
@@ -205,7 +207,7 @@ func (r *AccountRepository) GetSynthetic(ctx context.Context, limit int, offset
205207
var synthetic []SyntheticRow
206208
for rows.Next() {
207209
var s SyntheticRow
208-
if err := rows.Scan(&s.AccountID, &s.Name, &s.ReputationScore, &s.ReputationWeight); err != nil {
210+
if err := rows.Scan(&s.AccountID, &s.Name, &s.MTLAXBalance, &s.ReputationScore, &s.ReputationWeight); err != nil {
209211
return nil, fmt.Errorf("scan synthetic: %w", err)
210212
}
211213
synthetic = append(synthetic, s)
@@ -540,6 +542,7 @@ type SearchAccountRow struct {
540542
Name string
541543
MTLAPBalance float64
542544
MTLACBalance float64
545+
MTLAXBalance float64
543546
TotalXLMValue float64
544547
ReputationScore float64 // Weighted reputation score (0 if no ratings)
545548
ReputationWeight float64 // Total weight of raters
@@ -579,6 +582,7 @@ func (r *AccountRepository) SearchAccounts(ctx context.Context, query string, ta
579582
"COALESCE(m.data_value, CONCAT(LEFT(a.account_id, 6), '...', RIGHT(a.account_id, 6))) AS name",
580583
"a.mtlap_balance",
581584
"a.mtlac_balance",
585+
"COALESCE(a.mtlax_balance, 0) AS mtlax_balance",
582586
"a.total_xlm_value",
583587
"COALESCE(rs.weighted_score, 0) AS reputation_score",
584588
"COALESCE(rs.total_weight, 0) AS reputation_weight",
@@ -649,7 +653,7 @@ func (r *AccountRepository) SearchAccounts(ctx context.Context, query string, ta
649653
var accounts []SearchAccountRow
650654
for rows.Next() {
651655
var acc SearchAccountRow
652-
if err := rows.Scan(&acc.AccountID, &acc.Name, &acc.MTLAPBalance, &acc.MTLACBalance, &acc.TotalXLMValue, &acc.ReputationScore, &acc.ReputationWeight); err != nil {
656+
if err := rows.Scan(&acc.AccountID, &acc.Name, &acc.MTLAPBalance, &acc.MTLACBalance, &acc.MTLAXBalance, &acc.TotalXLMValue, &acc.ReputationScore, &acc.ReputationWeight); err != nil {
653657
return nil, fmt.Errorf("scan search account: %w", err)
654658
}
655659
accounts = append(accounts, acc)

internal/sync/accounts_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,42 @@ func TestFindBalance(t *testing.T) {
450450
}
451451
}
452452

453+
func TestGetMTLAXBalance(t *testing.T) {
454+
t.Run("no MTLAX trustline returns nil", func(t *testing.T) {
455+
data := &AccountData{
456+
Balances: []Balance{
457+
{AssetCode: "XLM", AssetIssuer: "", Balance: decimal.RequireFromString("100.0000000")},
458+
{AssetCode: "MTLAP", AssetIssuer: "GCNVDZIHGX473FEI7IXCUAEXUJ4BGCKEMHF36VYP5EMS7PX2QBLAMTLA", Balance: decimal.RequireFromString("1.0000000")},
459+
},
460+
}
461+
result := getMTLAXBalance(data)
462+
assert.Nil(t, result)
463+
})
464+
465+
t.Run("zero MTLAX balance returns pointer to zero", func(t *testing.T) {
466+
data := &AccountData{
467+
Balances: []Balance{
468+
{AssetCode: "MTLAX", AssetIssuer: "GCNVDZIHGX473FEI7IXCUAEXUJ4BGCKEMHF36VYP5EMS7PX2QBLAMTLA", Balance: decimal.Zero},
469+
},
470+
}
471+
result := getMTLAXBalance(data)
472+
assert.NotNil(t, result)
473+
assert.True(t, decimal.Zero.Equal(*result))
474+
})
475+
476+
t.Run("non-zero MTLAX balance returns pointer to balance", func(t *testing.T) {
477+
expected := decimal.RequireFromString("42.5000000")
478+
data := &AccountData{
479+
Balances: []Balance{
480+
{AssetCode: "MTLAX", AssetIssuer: "GCNVDZIHGX473FEI7IXCUAEXUJ4BGCKEMHF36VYP5EMS7PX2QBLAMTLA", Balance: expected},
481+
},
482+
}
483+
result := getMTLAXBalance(data)
484+
assert.NotNil(t, result)
485+
assert.True(t, expected.Equal(*result))
486+
})
487+
}
488+
453489
func TestFindBalanceEmptySlice(t *testing.T) {
454490
result := findBalance([]Balance{}, "XLM", "")
455491
assert.True(t, decimal.Zero.Equal(result))

internal/template/templates/home.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h2 class="section-title">Persons</h2>
8383
</div>
8484
{{if .HasMorePersons}}
8585
<div class="pagination">
86-
<a href="?persons_offset={{.NextPersonsOffset}}{{if .CorporateOffset}}&corporate_offset={{.CorporateOffset}}{{end}}#persons-section" class="btn">Load More &gt;</a>
86+
<a href="?persons_offset={{.NextPersonsOffset}}{{if .SyntheticOffset}}&synthetic_offset={{.SyntheticOffset}}{{end}}{{if .CorporateOffset}}&corporate_offset={{.CorporateOffset}}{{end}}#persons-section" class="btn">Load More &gt;</a>
8787
</div>
8888
{{end}}
8989
{{else}}
@@ -169,7 +169,7 @@ <h2 class="section-title">Corporate</h2>
169169
</div>
170170
{{if .HasMoreCorporate}}
171171
<div class="pagination">
172-
<a href="?{{if .PersonsOffset}}persons_offset={{.PersonsOffset}}&{{end}}corporate_offset={{.NextCorporateOffset}}#corporate-section" class="btn">Load More &gt;</a>
172+
<a href="?{{if .PersonsOffset}}persons_offset={{.PersonsOffset}}&{{end}}{{if .SyntheticOffset}}synthetic_offset={{.SyntheticOffset}}&{{end}}corporate_offset={{.NextCorporateOffset}}#corporate-section" class="btn">Load More &gt;</a>
173173
</div>
174174
{{end}}
175175
{{else}}

internal/template/templates/search.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
</td>
128128
<td>
129129
{{if $acc.IsPerson}}<span class="type-badge type-person">[MTLAP]</span>{{end}}
130+
{{if $acc.IsSynthetic}}<span class="type-badge mtlax">[MTLAX]</span>{{end}}
130131
{{if $acc.IsCorporate}}<span class="type-badge type-corporate">[MTLAC]</span>{{end}}
131132
</td>
132133
<td class="cell-num">

0 commit comments

Comments
 (0)