Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion chessfut-be/application/service/refresh_card_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
29 changes: 29 additions & 0 deletions chessfut-be/application/service/refresh_card_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
26 changes: 26 additions & 0 deletions chessfut-fe/src/components/compare/ComparisonTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="grid grid-cols-3 items-center gap-4 py-1 text-sm">
<span className="text-right text-white/70">{a || "—"}</span>
<span className="text-center text-white/50">{label}</span>
<span className="text-left text-white/70">{b || "—"}</span>
</div>
);
}

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 (
<div className="flex w-full max-w-xs flex-col gap-1 rounded-xl border border-white/10 bg-neutral-800 p-4">
<TextRow label="Title" a={cardA.title} b={cardB.title} />
<TextRow label="FIDE" a={fideDisplay(cardA)} b={fideDisplay(cardB)} />
<div className="my-2 h-px bg-white/10" />
<Row label="OVR" a={cardA.ovr} b={cardB.ovr} />
<Row label="PAC" a={cardA.attributes.pac} b={cardB.attributes.pac} />
<Row label="SHO" a={cardA.attributes.sho} b={cardB.attributes.sho} />
Expand All @@ -28,6 +51,9 @@ export function ComparisonTable({ cardA, cardB }: { cardA: Card; cardB: Card })
<Row label="Blitz" a={cardA.blitz.rating} b={cardB.blitz.rating} />
<Row label="Rapid" a={cardA.rapid.rating} b={cardB.rapid.rating} />
<Row label="Daily" a={cardA.daily.rating} b={cardB.daily.rating} />
{(cardA.fide_source === "title_default" || cardB.fide_source === "title_default") && (
<p className="mt-2 text-center text-[10px] text-white/30">* FIDE not linked — title minimum assumed</p>
)}
</div>
);
}
Loading