From 856db73ffbdce5160f02a3bcf4946dca04a33b6e Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:13:39 +0300 Subject: [PATCH] Fix UserCard not honoring the unverified peak XP div selected --- .../UserCardRepository.server.test.ts | 50 ++++++++++++++++++ .../user-card/UserCardRepository.server.ts | 51 ++++++++++++------- .../actions/user-card.edit.server.ts | 18 ++++--- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/app/features/user-card/UserCardRepository.server.test.ts b/app/features/user-card/UserCardRepository.server.test.ts index 38eabb952..a0faf01a6 100644 --- a/app/features/user-card/UserCardRepository.server.test.ts +++ b/app/features/user-card/UserCardRepository.server.test.ts @@ -237,6 +237,56 @@ describe("UserCardRepository.findAllByUserIds", () => { }); }); + it("surfaces self-reported peak XP equal to the other division's verified peak", async () => { + await insertVerifiedXp(owner.id, 3100, "WEST"); + await insertVerifiedXp(owner.id, 3000, "JPN"); + await withUserId(owner.id, () => + UserCardRepository.updateOwnCard({ + shortBio: null, + bannerPresetImg: null, + bannerImgId: null, + unverifiedPeakXP: { overall: 3100, takoroka: 3100, tentatek: null }, + xpDivision: "JPN", + hiddenCardStats: [], + }), + ); + + const { userCards } = await withNoUser(() => + UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + ); + + expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + type: "XP", + values: [ + { isVerified: false, region: "JPN", points: 3100 }, + { isVerified: true, region: "JPN", points: 3000 }, + ], + }); + }); + + it("keeps the self-reported peak XP in the picked division without placements there", async () => { + await insertVerifiedXp(owner.id, 2639.5, "WEST"); + await withUserId(owner.id, () => + UserCardRepository.updateOwnCard({ + shortBio: null, + bannerPresetImg: null, + bannerImgId: null, + unverifiedPeakXP: { overall: 2650, takoroka: 2650, tentatek: null }, + xpDivision: "JPN", + hiddenCardStats: [], + }), + ); + + const { userCards } = await withNoUser(() => + UserCardRepository.findAllByUserIds({ userIds: [owner.id] }), + ); + + expect(findXpStat(userCards.get(owner.id))).toMatchObject({ + type: "XP", + values: [{ isVerified: false, region: "JPN", points: 2650 }], + }); + }); + it("ignores self-reported peak XP when there is no verified XP", async () => { await withUserId(owner.id, () => UserCardRepository.updateOwnCard({ diff --git a/app/features/user-card/UserCardRepository.server.ts b/app/features/user-card/UserCardRepository.server.ts index 71ba4b39a..342fa86de 100644 --- a/app/features/user-card/UserCardRepository.server.ts +++ b/app/features/user-card/UserCardRepository.server.ts @@ -516,8 +516,7 @@ function xpPeaksJson(eb: ExpressionBuilder) { } /** - * Self-reported peak XP from the `User.unverifiedPeakXP` column. Its division is the one the - * verified XP resolved to, since a claim only counts as one made on top of that value. + * Self-reported peak XP from the `User.unverifiedPeakXP` column. */ function xpUnverifiedPointsScalar() { return sql`"User"."unverifiedPeakXP" ->> '$.overall'`; @@ -645,6 +644,7 @@ function enrichUserCardData( const stats = userCardStats({ div: cardData.div, plusTier: cardData.plusTier, + xpDivision: cardData.xpDivision, xpVerified: verifiedXp(cardData.xpPeaks, cardData.xpDivision), xpUnverifiedPoints: cardData.xpUnverifiedPoints, seasonSkill, @@ -714,6 +714,7 @@ function enrichBanner( function userCardStats({ div, plusTier, + xpDivision, xpVerified, xpUnverifiedPoints, seasonSkill, @@ -721,6 +722,7 @@ function userCardStats({ }: { div: string | null; plusTier: number | null; + xpDivision: XRankPlacementRegion | null; xpVerified: { points: number; region: XRankPlacementRegion } | null; xpUnverifiedPoints: number | null; seasonSkill: TieredSkill | undefined; @@ -729,26 +731,22 @@ function userCardStats({ const stats: Array = []; if (xpVerified) { - const xpValues: Array = []; - // self-reported peak XP is only surfaced as a valid claim sitting on top of a verified placement - if ( - xpUnverifiedPoints !== null && - isValidUnverifiedXp({ - unverified: xpUnverifiedPoints, - verified: xpVerified.points, - }) - ) { + const unverified = unverifiedXpValue({ + points: xpUnverifiedPoints, + region: xpDivision ?? xpVerified.region, + verifiedPoints: xpVerified.points, + }); + + const xpValues: Array = unverified ? [unverified] : []; + // the verified peak joins the claim only when it is from the division the claim was made in; + // in another division it is a peak on a ladder the card is not about + if (!unverified || unverified.region === xpVerified.region) { xpValues.push({ - isVerified: false, + isVerified: true, region: xpVerified.region, - points: xpUnverifiedPoints, + points: xpVerified.points, }); } - xpValues.push({ - isVerified: true, - region: xpVerified.region, - points: xpVerified.points, - }); stats.push({ type: "XP", values: xpValues }); } @@ -767,3 +765,20 @@ function userCardStats({ return stats; } + +function unverifiedXpValue({ + points, + region, + verifiedPoints, +}: { + points: number | null; + region: XRankPlacementRegion; + verifiedPoints: number; +}): UserCardStatXPValue | null { + if (points === null) return null; + if (!isValidUnverifiedXp({ unverified: points, verified: verifiedPoints })) { + return null; + } + + return { isVerified: false, region, points }; +} diff --git a/app/features/user-card/actions/user-card.edit.server.ts b/app/features/user-card/actions/user-card.edit.server.ts index 6b45973b1..83617ccef 100644 --- a/app/features/user-card/actions/user-card.edit.server.ts +++ b/app/features/user-card/actions/user-card.edit.server.ts @@ -1,5 +1,7 @@ import { type ActionFunction, redirect } from "react-router"; +import type { PeakXP } from "~/db/tables-json"; import { requireUser } from "~/features/auth/core/user.server"; +import type { XRankPlacementRegion } from "~/features/top-search/top-search-types"; import { parseFormDataWithImages } from "~/form/parse.server"; import { userPage } from "~/utils/urls"; import * as UserCardRepository from "../UserCardRepository.server"; @@ -50,13 +52,7 @@ export const action: ActionFunction = async ({ request }) => { xpDivision: data.xpDivision, unverifiedPeakXP: data.unverifiedXpPoints && verifiedXp - ? { - overall: data.unverifiedXpPoints, - tentatek: - verifiedXp.region === "WEST" ? data.unverifiedXpPoints : null, - takoroka: - verifiedXp.region === "JPN" ? data.unverifiedXpPoints : null, - } + ? peakXP(data.unverifiedXpPoints, data.xpDivision ?? verifiedXp.region) : null, hiddenCardStats: resolveHiddenStats(data), }); @@ -64,6 +60,14 @@ export const action: ActionFunction = async ({ request }) => { throw redirect(returnTo ?? userPage(user)); }; +function peakXP(points: number, region: XRankPlacementRegion): PeakXP { + return { + overall: points, + tentatek: region === "WEST" ? points : null, + takoroka: region === "JPN" ? points : null, + }; +} + function resolveBanner({ bannerType, bannerColor,