TopAwards on SponsorFamilyListItem — public top-4 family prizes/skills aggregation #871

Closed
opened 2026-07-27 14:29:43 +00:00 by spikerj · 1 comment
Owner

Part of #868 (PR-3, backend).

FundPrizeAward and EarnedSkill are keyed by Keycloak UserId, which is never on the sponsor wire — so the card cannot join client-side. Add the join server-side where the profiles are already in hand:

  • SponsorDtos.cs: SponsorFamilyAward { Icon, Title, Kind ("prize"|"skill"), Tier?, MemberDisplayName?, EarnedAt }; SponsorFamilyListItem.TopAwards (≤4, capped server-side like TopDestinations.Take(5)).
  • SponsorFamilyBuilder: accept pre-fetched prizesByUser / skillsByUser dictionaries (same shape as donationsByProfile). Ranking: fund prizes by ThresholdCents desc first, then skills gold > silver > bronze, newest-first tiebreak.
  • SponsorController.LoadPublicSponsorContextAsync: batch-load FundPrizeAward+FundPrize, EarnedSkill+SkillDefinition for listed members only.
  • Privacy: never emit Keycloak ids; attribution via DisplayName only; no awards for members not publicly listed.
  • Tests in the SponsorFamilyBuilder suite: ranking order, 4-cap, privacy.
Part of #868 (PR-3, backend). `FundPrizeAward` and `EarnedSkill` are keyed by Keycloak UserId, which is never on the sponsor wire — so the card cannot join client-side. Add the join server-side where the profiles are already in hand: - `SponsorDtos.cs`: `SponsorFamilyAward { Icon, Title, Kind ("prize"|"skill"), Tier?, MemberDisplayName?, EarnedAt }`; `SponsorFamilyListItem.TopAwards` (≤4, capped server-side like `TopDestinations.Take(5)`). - `SponsorFamilyBuilder`: accept pre-fetched `prizesByUser` / `skillsByUser` dictionaries (same shape as `donationsByProfile`). Ranking: fund prizes by `ThresholdCents` desc first, then skills gold > silver > bronze, newest-first tiebreak. - `SponsorController.LoadPublicSponsorContextAsync`: batch-load `FundPrizeAward`+`FundPrize`, `EarnedSkill`+`SkillDefinition` for listed members only. - Privacy: never emit Keycloak ids; attribution via `DisplayName` only; no awards for members not publicly listed. - Tests in the `SponsorFamilyBuilder` suite: ranking order, 4-cap, privacy.
Author
Owner

Resolved in spikersoft-backend PR #497 (consolidated family-card epic PR, c8f692d7), with test hardening in PR #500. The standalone PR #494 was closed unmerged in favour of the consolidation. Verified against origin/master:

  • SponsorDtos.cs:106TopAwards; :113-129SponsorFamilyAward carrying all six fields (Icon, Title, Kind, Tier?, MemberDisplayName?, EarnedAt).
  • SponsorFamilyBuilder.cs:259-300BuildTopAwards: prizes first, then ThresholdCents desc / TierRank, ThenByDescending(EarnedAt), .Take(4).
  • Privacy boundary holds: attribution is by members[i].DisplayName only — no user id or profile id crosses into the public payload.
  • Batch load via SponsorController.LoadAwardsByUserAsync (prize awards + prizes + skills, filtered to the listed members' Keycloak ids) — no N+1.
  • Tests SponsorFamilyBuilderTests.cs:375 (ranking + 4-cap + display-name privacy), :396 (empty), :406 (out-of-family awards ignored), :421 (newest-wins tiebreak). Confirmed these ranking assertions survived the #882 store-mock rework rather than being weakened by it.

Closing.

Resolved in spikersoft-backend PR #497 (consolidated family-card epic PR, `c8f692d7`), with test hardening in PR #500. The standalone PR #494 was closed unmerged in favour of the consolidation. Verified against `origin/master`: - `SponsorDtos.cs:106` — `TopAwards`; `:113-129` — `SponsorFamilyAward` carrying all six fields (`Icon`, `Title`, `Kind`, `Tier?`, `MemberDisplayName?`, `EarnedAt`). - `SponsorFamilyBuilder.cs:259-300` — `BuildTopAwards`: prizes first, then `ThresholdCents` desc / `TierRank`, `ThenByDescending(EarnedAt)`, `.Take(4)`. - **Privacy boundary holds**: attribution is by `members[i].DisplayName` only — no user id or profile id crosses into the public payload. - Batch load via `SponsorController.LoadAwardsByUserAsync` (prize awards + prizes + skills, filtered to the listed members' Keycloak ids) — no N+1. - Tests `SponsorFamilyBuilderTests.cs:375` (ranking + 4-cap + display-name privacy), `:396` (empty), `:406` (out-of-family awards ignored), `:421` (newest-wins tiebreak). Confirmed these ranking assertions survived the #882 store-mock rework rather than being weakened by it. Closing.
Sign in to join this conversation.