Fix UserCard not honoring the unverified peak XP div selected

This commit is contained in:
Kalle
2026-08-11 19:13:39 +03:00
parent 6de5e55e41
commit 856db73ffb
3 changed files with 94 additions and 25 deletions

View File

@@ -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({

View File

@@ -516,8 +516,7 @@ function xpPeaksJson(eb: ExpressionBuilder<Tables, "User">) {
}
/**
* 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<number | null>`"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<UserCardStat> = [];
if (xpVerified) {
const xpValues: Array<UserCardStatXPValue> = [];
// 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<UserCardStatXPValue> = 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 };
}

View File

@@ -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,