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:
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.
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).
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #868 (PR-3, backend).
FundPrizeAwardandEarnedSkillare 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 likeTopDestinations.Take(5)).SponsorFamilyBuilder: accept pre-fetchedprizesByUser/skillsByUserdictionaries (same shape asdonationsByProfile). Ranking: fund prizes byThresholdCentsdesc first, then skills gold > silver > bronze, newest-first tiebreak.SponsorController.LoadPublicSponsorContextAsync: batch-loadFundPrizeAward+FundPrize,EarnedSkill+SkillDefinitionfor listed members only.DisplayNameonly; no awards for members not publicly listed.SponsorFamilyBuildersuite: ranking order, 4-cap, privacy.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 againstorigin/master:SponsorDtos.cs:106—TopAwards;:113-129—SponsorFamilyAwardcarrying all six fields (Icon,Title,Kind,Tier?,MemberDisplayName?,EarnedAt).SponsorFamilyBuilder.cs:259-300—BuildTopAwards: prizes first, thenThresholdCentsdesc /TierRank,ThenByDescending(EarnedAt),.Take(4).members[i].DisplayNameonly — no user id or profile id crosses into the public payload.SponsorController.LoadAwardsByUserAsync(prize awards + prizes + skills, filtered to the listed members' Keycloak ids) — no N+1.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.