From dcb6eb0ab1981b8f3c39d9469ea229284c43be4f Mon Sep 17 00:00:00 2001 From: Fayupable <90789180+Fayupable@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:43:19 +0300 Subject: [PATCH] feat: enhance refresh card service to preserve existing card type and improve test coverage --- .../service/refresh_card_service.go | 10 ++++++- .../service/refresh_card_service_test.go | 29 +++++++++++++++++++ .../service/refresh_stale_cards_service.go | 8 ++--- .../components/compare/ComparisonTable.tsx | 26 +++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/chessfut-be/application/service/refresh_card_service.go b/chessfut-be/application/service/refresh_card_service.go index f03e7f9..b87eb99 100644 --- a/chessfut-be/application/service/refresh_card_service.go +++ b/chessfut-be/application/service/refresh_card_service.go @@ -24,8 +24,16 @@ func NewRefreshCardService( var _ input.RefreshCardUseCase = (*RefreshCardService)(nil) +// Execute rebuilds the card as whatever type it already was (fast or +// detailed) — defaulting to fast only when no card exists yet — so an admin +// refresh can never silently downgrade a detailed card to fast. func (s *RefreshCardService) Execute(ctx context.Context, username string) (domain.Card, error) { - card, err := buildFastCard(ctx, s.chessComClient, username) + cardType := domain.CardTypeFast + if existing, found, err := s.cardRepository.FindByUsername(ctx, username); err == nil && found { + cardType = existing.CardType + } + + card, err := buildCardByType(ctx, s.chessComClient, username, cardType) if err != nil { return domain.Card{}, err } diff --git a/chessfut-be/application/service/refresh_card_service_test.go b/chessfut-be/application/service/refresh_card_service_test.go index ec7c51b..4ac09ce 100644 --- a/chessfut-be/application/service/refresh_card_service_test.go +++ b/chessfut-be/application/service/refresh_card_service_test.go @@ -18,6 +18,7 @@ func TestRefreshCardService_Execute(t *testing.T) { client := new(mockChessComClient) repo := new(mockCardRepository) cache := new(mockCache) + repo.On("FindByUsername", ctx, "hikaru").Return(domain.Card{}, false, nil) client.On("GetProfile", ctx, "hikaru").Return(player, nil) client.On("GetStats", ctx, "hikaru").Return(stats, nil) repo.On("Save", ctx, mock.AnythingOfType("domain.Card")).Return(nil) @@ -35,12 +36,39 @@ func TestRefreshCardService_Execute(t *testing.T) { cache.AssertExpectations(t) } +func TestRefreshCardService_Execute_PreservesDetailedCardType(t *testing.T) { + ctx := context.Background() + player := domain.Player{Username: "hikaru", Title: domain.TitleGM} + stats := domain.PlayerStats{FideRating: 2814, Blitz: domain.TimeControlStats{Rating: 3414}, Rapid: domain.TimeControlStats{Rating: 2839}} + existing := domain.Card{Player: player, CardType: domain.CardTypeDetailed} + + client := new(mockChessComClient) + repo := new(mockCardRepository) + cache := new(mockCache) + repo.On("FindByUsername", ctx, "hikaru").Return(existing, true, nil) + client.On("GetProfile", ctx, "hikaru").Return(player, nil) + client.On("GetStats", ctx, "hikaru").Return(stats, nil) + client.On("GetGames", ctx, "hikaru", mock.Anything, mock.Anything).Return([]domain.Game{}, nil) + repo.On("Save", ctx, mock.AnythingOfType("domain.Card")).Return(nil) + cache.On("SetCard", ctx, mock.AnythingOfType("domain.Card")).Return(nil) + + svc := NewRefreshCardService(client, repo, cache) + result, err := svc.Execute(ctx, "hikaru") + + assert.NoError(t, err) + assert.Equal(t, domain.CardTypeDetailed, result.CardType) + client.AssertExpectations(t) + repo.AssertExpectations(t) + cache.AssertExpectations(t) +} + func TestRefreshCardService_Execute_ReturnsErrorWhenChessComFails(t *testing.T) { ctx := context.Background() client := new(mockChessComClient) repo := new(mockCardRepository) cache := new(mockCache) + repo.On("FindByUsername", ctx, "unknown").Return(domain.Card{}, false, nil) client.On("GetProfile", ctx, "unknown").Return(domain.Player{}, assert.AnError) svc := NewRefreshCardService(client, repo, cache) @@ -58,6 +86,7 @@ func TestRefreshCardService_Execute_ReturnsErrorWhenSaveFails(t *testing.T) { client := new(mockChessComClient) repo := new(mockCardRepository) cache := new(mockCache) + repo.On("FindByUsername", ctx, "hikaru").Return(domain.Card{}, false, nil) client.On("GetProfile", ctx, "hikaru").Return(player, nil) client.On("GetStats", ctx, "hikaru").Return(stats, nil) repo.On("Save", ctx, mock.AnythingOfType("domain.Card")).Return(assert.AnError) diff --git a/chessfut-be/application/service/refresh_stale_cards_service.go b/chessfut-be/application/service/refresh_stale_cards_service.go index dfd5436..04acd54 100644 --- a/chessfut-be/application/service/refresh_stale_cards_service.go +++ b/chessfut-be/application/service/refresh_stale_cards_service.go @@ -44,7 +44,7 @@ func (s *RefreshStaleCardsService) Execute(ctx context.Context, batchSize int) ( continue } - rebuilt, err := s.buildCardByType(ctx, existing.Player.Username, existing.CardType) + rebuilt, err := buildCardByType(ctx, s.chessComClient, existing.Player.Username, existing.CardType) if err != nil { continue } @@ -55,9 +55,9 @@ func (s *RefreshStaleCardsService) Execute(ctx context.Context, batchSize int) ( return refreshed, nil } -func (s *RefreshStaleCardsService) buildCardByType(ctx context.Context, username string, cardType domain.CardType) (domain.Card, error) { +func buildCardByType(ctx context.Context, client output.ChessComClientPort, username string, cardType domain.CardType) (domain.Card, error) { if cardType == domain.CardTypeDetailed { - return buildDetailedCard(ctx, s.chessComClient, username) + return buildDetailedCard(ctx, client, username) } - return buildFastCard(ctx, s.chessComClient, username) + return buildFastCard(ctx, client, username) } diff --git a/chessfut-fe/src/components/compare/ComparisonTable.tsx b/chessfut-fe/src/components/compare/ComparisonTable.tsx index 660d4d4..ce6b3e3 100644 --- a/chessfut-fe/src/components/compare/ComparisonTable.tsx +++ b/chessfut-fe/src/components/compare/ComparisonTable.tsx @@ -13,9 +13,32 @@ function Row({ label, a, b }: { label: string; a: number; b: number }) { ); } +function TextRow({ label, a, b }: { label: string; a: string; b: string }) { + return ( +
+ {a || "—"} + {label} + {b || "—"} +
+ ); +} + +function fideDisplay(card: Card): string { + if (card.fide_source === "verified" && card.fide_rating) { + return String(card.fide_rating); + } + if (card.fide_source === "title_default" && card.effective_fide_rating) { + return `${card.effective_fide_rating}*`; + } + return "—"; +} + export function ComparisonTable({ cardA, cardB }: { cardA: Card; cardB: Card }) { return (
+ + +
@@ -28,6 +51,9 @@ export function ComparisonTable({ cardA, cardB }: { cardA: Card; cardB: Card }) + {(cardA.fide_source === "title_default" || cardB.fide_source === "title_default") && ( +

* FIDE not linked — title minimum assumed

+ )}
); } \ No newline at end of file