Skip to content

Commit dec48d2

Browse files
committed
Merge branch 'develop' of github.com:topcoder-platform/member-api-v6 into develop
2 parents 036e2c5 + 232567e commit dec48d2

2 files changed

Lines changed: 170 additions & 2 deletions

File tree

src/common/prismaHelper.js

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,101 @@ function getUnifiedTypeName (typeId) {
8585
return canonical || typeId
8686
}
8787

88+
/**
89+
* Normalize a challenge id used to match duplicate history rows across tracks.
90+
* @param {*} value raw challenge id from a memberStatsHistory response row
91+
* @returns {string|undefined} normalized challenge id when available
92+
*/
93+
function normalizeHistoryChallengeId (value) {
94+
if (_.isNil(value)) {
95+
return undefined
96+
}
97+
98+
const challengeId = String(value).trim()
99+
return challengeId || undefined
100+
}
101+
102+
/**
103+
* Normalize a history placement so only positive integer placements are copied.
104+
* @param {*} value raw placement value from a history row
105+
* @returns {number|undefined} visible placement when available
106+
*/
107+
function normalizeHistoryPlacement (value) {
108+
const placement = _.toInteger(value)
109+
return Number.isInteger(placement) && placement > 0 ? placement : undefined
110+
}
111+
112+
/**
113+
* Return the rating value that a history card can display.
114+
* @param {Object} row unified history row
115+
* @returns {*} newRating or rating when either value is available
116+
*/
117+
function getHistoryDisplayRating (row) {
118+
return _.isNil(row && row.newRating) ? row && row.rating : row.newRating
119+
}
120+
121+
/**
122+
* Fill missing display fields on duplicate challenge history rows.
123+
*
124+
* A completed challenge can appear once under its source track and again under a
125+
* configured rating path, for example DEVELOPMENT / Challenge and DATA_SCIENCE /
126+
* AI Engineering. The source-track row may only have the challenge card metadata,
127+
* while the rating-path row carries the placement and rating needed by profile
128+
* details. Existing values are preserved and only missing placement/newRating
129+
* fields are copied.
130+
*
131+
* @param {Array<Object>} rows unified history rows
132+
* @returns {Array<Object>} history rows with duplicate display fields backfilled
133+
*/
134+
function fillMissingHistoryDisplayFields (rows) {
135+
const displayFieldsByChallengeId = new Map()
136+
137+
_.forEach(rows || [], (row) => {
138+
const challengeId = normalizeHistoryChallengeId(row && row.challengeId)
139+
if (!challengeId) {
140+
return
141+
}
142+
143+
const placement = normalizeHistoryPlacement(row.placement)
144+
const rating = getHistoryDisplayRating(row)
145+
if (!placement && _.isNil(rating)) {
146+
return
147+
}
148+
149+
const existing = displayFieldsByChallengeId.get(challengeId) || {}
150+
displayFieldsByChallengeId.set(challengeId, {
151+
placement: placement && (!existing.placement || placement < existing.placement)
152+
? placement
153+
: existing.placement,
154+
rating: _.isNil(existing.rating) ? rating : existing.rating
155+
})
156+
})
157+
158+
return _.map(rows || [], (row) => {
159+
const challengeId = normalizeHistoryChallengeId(row && row.challengeId)
160+
const displayFields = challengeId ? displayFieldsByChallengeId.get(challengeId) : undefined
161+
if (!displayFields) {
162+
return row
163+
}
164+
165+
const existingPlacement = normalizeHistoryPlacement(row.placement)
166+
const placement = existingPlacement || displayFields.placement
167+
const newRating = _.isNil(row.newRating) && !_.isNil(displayFields.rating)
168+
? displayFields.rating
169+
: row.newRating
170+
171+
if (placement === existingPlacement && newRating === row.newRating) {
172+
return row
173+
}
174+
175+
return {
176+
...row,
177+
placement,
178+
newRating
179+
}
180+
})
181+
}
182+
88183
function isUuidValue (value) {
89184
return uuidPattern.test(String(value || '').trim())
90185
}
@@ -823,15 +918,16 @@ function buildUnifiedStatsHistoryResponse (member, historyStats, fields) {
823918
}))
824919
.filter(row => _.includes(supportedUnifiedHistoryTrackNames, row.resolvedTrackName))
825920
.value()
826-
const first = _.head(validRows) || {}
921+
const displayRows = fillMissingHistoryDisplayFields(validRows)
922+
const first = _.head(displayRows) || {}
827923
const item = {
828924
userId: helper.bigIntToNumber(member.userId),
829925
groupId: helper.bigIntToNumber(first.groupId),
830926
handle: member.handle,
831927
handleLower: member.handleLower
832928
}
833929

834-
const groupedByTrackType = _.groupBy(validRows, row => `${row.resolvedTrackName}::${row.resolvedTypeName}`)
930+
const groupedByTrackType = _.groupBy(displayRows, row => `${row.resolvedTrackName}::${row.resolvedTypeName}`)
835931
_.forEach(groupedByTrackType, (trackHistory, key) => {
836932
const [trackName, typeName] = key.split('::')
837933
if (_.includes(groupedSubTrackStatsTrackNames, trackName)) {

test/unit/StatsDimensionHelper.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,78 @@ describe('stats dimension helper unit tests', () => {
329329
result.DEVELOP.subTracks[0].history[0].ratingDate.should.equal(ratingDate.getTime())
330330
})
331331

332+
it('buildUnifiedStatsHistoryResponse should fill missing challenge history display fields from duplicate rating paths', () => {
333+
const firstRatingDate = new Date('2026-05-01T00:00:00.000Z')
334+
const secondRatingDate = new Date('2026-05-02T00:00:00.000Z')
335+
const result = prismaHelper.buildUnifiedStatsHistoryResponse(
336+
{
337+
userId: global.BigInt(15391415),
338+
handle: 'winterflame',
339+
handleLower: 'winterflame'
340+
},
341+
[
342+
{
343+
groupId: global.BigInt(1),
344+
trackId: 'track-dev-id',
345+
typeId: 'type-challenge-id',
346+
trackName: TRACK_NAMES.DEVELOP,
347+
typeName: TYPE_NAMES.CHALLENGE,
348+
challengeId: 'ai-profile-pipeline',
349+
challengeName: 'AI-Powered Topcoder Member Profile Video Pipeline',
350+
eventDate: firstRatingDate,
351+
mostRecent: false
352+
},
353+
{
354+
groupId: global.BigInt(1),
355+
trackId: 'track-ds-id',
356+
typeId: 'rating-path-ai-engineering',
357+
trackName: TRACK_NAMES.DATA_SCIENCE,
358+
typeName: 'AI Engineering',
359+
challengeId: 'ai-profile-pipeline',
360+
challengeName: 'AI-Powered Topcoder Member Profile Video Pipeline',
361+
newRating: 1180,
362+
placement: 4,
363+
eventDate: firstRatingDate,
364+
mostRecent: false
365+
},
366+
{
367+
groupId: global.BigInt(1),
368+
trackId: 'track-dev-id',
369+
typeId: 'type-challenge-id',
370+
trackName: TRACK_NAMES.DEVELOP,
371+
typeName: TYPE_NAMES.CHALLENGE,
372+
challengeId: 'cognitive-diplomat',
373+
challengeName: 'Cognitive Diplomat: Negotiation Next-Turn Predictor',
374+
newRating: 1357,
375+
eventDate: secondRatingDate,
376+
mostRecent: true
377+
},
378+
{
379+
groupId: global.BigInt(1),
380+
trackId: 'track-ds-id',
381+
typeId: 'rating-path-ai-engineering',
382+
trackName: TRACK_NAMES.DATA_SCIENCE,
383+
typeName: 'AI Engineering',
384+
challengeId: 'cognitive-diplomat',
385+
challengeName: 'Cognitive Diplomat: Negotiation Next-Turn Predictor',
386+
newRating: 1225,
387+
placement: 5,
388+
eventDate: secondRatingDate,
389+
mostRecent: true
390+
}
391+
]
392+
)
393+
394+
const developmentHistory = result.DEVELOP.subTracks[0].history
395+
developmentHistory[0].newRating.should.equal(1180)
396+
developmentHistory[0].rating.should.equal(1180)
397+
developmentHistory[0].placement.should.equal(4)
398+
developmentHistory[1].newRating.should.equal(1357)
399+
developmentHistory[1].rating.should.equal(1357)
400+
developmentHistory[1].placement.should.equal(5)
401+
result.DATA_SCIENCE['AI Engineering'].history[0].placement.should.equal(4)
402+
})
403+
332404
it('buildUnifiedStatsHistoryResponse should expose QA challenge history', () => {
333405
const ratingDate = new Date('2026-06-15T08:00:00.000Z')
334406
const result = prismaHelper.buildUnifiedStatsHistoryResponse(

0 commit comments

Comments
 (0)