From 9f5c6bf65f33583f69fa145772db613c7fbc4f18 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Wed, 27 May 2026 13:15:46 +1000 Subject: [PATCH] PM-5161: Exclude example scores from MM provisional display What was broken Community App grouped Marathon Match submissions could show an example-test review summation as the provisional score when the example score was newer than the real provisional summation. That made CA disagree with Work Manager for members whose earlier submission failed and whose latest submission had separate example, provisional, and system summations. Root cause (if identifiable) buildMmSubmissionData treated every non-final review summation as provisional. Example summations are also non-final, so their aggregate scores could overwrite the real provisional score during submission row aggregation. What was changed Classify example review summations separately and keep them attached to submission history without letting them update provisional or final display scores. Final scores now update only from final summations, while legacy untyped non-final summations still fall back to provisional. Any added/updated tests Added a Marathon Match submission aggregation test covering system, example, and provisional summations on one latest submission, verifying the example score is retained but not displayed as provisional or final. --- .../shared/utils/mm-review-summations.test.js | 68 +++++++++++++++++++ src/shared/utils/mm-review-summations.js | 29 +++++--- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/__tests__/shared/utils/mm-review-summations.test.js b/__tests__/shared/utils/mm-review-summations.test.js index 02c4524c9..92e32c57f 100644 --- a/__tests__/shared/utils/mm-review-summations.test.js +++ b/__tests__/shared/utils/mm-review-summations.test.js @@ -182,4 +182,72 @@ describe('buildMmSubmissionData', () => { }), ]); }); + + it('does not treat example summations as provisional or final scores', () => { + const reviewSummations = [ + { + aggregateScore: 73.14798973137148, + id: 'summation-system', + isFinal: true, + metadata: { + testType: 'system', + }, + reviewedDate: '2026-05-26T06:52:08.038Z', + submissionId: 'submission-latest', + submitterHandle: 'topacc_four', + submitterId: '1004', + }, + { + aggregateScore: 76.16139684222824, + id: 'summation-example', + isExample: true, + metadata: { + testType: 'example', + }, + reviewedDate: '2026-05-26T06:04:39.123Z', + submissionId: 'submission-latest', + submitterHandle: 'topacc_four', + submitterId: '1004', + }, + { + aggregateScore: 69.13482014723114, + id: 'summation-provisional', + isProvisional: true, + metadata: { + testType: 'provisional', + }, + reviewedDate: '2026-05-26T06:03:55.171Z', + submissionId: 'submission-latest', + submitterHandle: 'topacc_four', + submitterId: '1004', + }, + ]; + + const rawSubmissions = [ + { + createdAt: '2026-05-26T06:02:59.385Z', + id: 'submission-latest', + isLatest: true, + memberId: '1004', + registrant: { + memberHandle: 'topacc_four', + memberId: '1004', + }, + }, + ]; + + const result = buildMmSubmissionData(reviewSummations, rawSubmissions); + + expect(result).toHaveLength(1); + expect(result[0].submissions).toEqual([ + expect.objectContaining({ + finalScore: 73.14798973137148, + provisionalScore: 69.13482014723114, + reviewSummations: expect.arrayContaining([ + expect.objectContaining({ id: 'summation-example' }), + ]), + submissionId: 'submission-latest', + }), + ]); + }); }); diff --git a/src/shared/utils/mm-review-summations.js b/src/shared/utils/mm-review-summations.js index e29b476af..caf58f6b4 100644 --- a/src/shared/utils/mm-review-summations.js +++ b/src/shared/utils/mm-review-summations.js @@ -28,21 +28,29 @@ function getSummationScoreClassification(summation) { const type = _.toLower(_.toString(_.get(summation, 'type', '')).trim()); const stage = _.toLower(_.toString(_.get(metadata, 'stage', '')).trim()); const testType = _.toLower(_.toString(_.get(metadata, 'testType', '')).trim()); - - const isProvisional = Boolean( + const isExample = Boolean( + _.get(summation, 'isExample') + || _.get(summation, 'is_example') + || type === 'example' + || testType === 'example', + ); + const hasProvisionalMarker = Boolean( _.get(summation, 'isProvisional') || _.get(summation, 'is_provisional') || type === 'provisional' || testType === 'provisional', ); - const isFinal = Boolean( + const hasFinalMarker = Boolean( _.get(summation, 'isFinal') || _.get(summation, 'is_final') || type === 'final' || stage === 'final', ); + const isProvisional = !isExample && hasProvisionalMarker; + const isFinal = !isExample && hasFinalMarker; return { + isExample, isProvisional, isFinal, }; @@ -263,6 +271,7 @@ function updateSubmissionEntry( timestampValue, normalizedScore, summation, + isFinal, isProvisional, isLatest, }, @@ -302,14 +311,14 @@ function updateSubmissionEntry( ) : { meta: baseEntry.provisionalMeta, value: baseEntry.provisionalScore }; - const finalResult = isProvisional - ? { meta: baseEntry.finalMeta, value: baseEntry.finalScore } - : mergeScoreData( + const finalResult = isFinal + ? mergeScoreData( baseEntry.finalMeta, baseEntry.finalScore, normalizedScore, timestampValue, - ); + ) + : { meta: baseEntry.finalMeta, value: baseEntry.finalScore }; const reviewSummations = [...baseEntry.reviewSummations, summation]; @@ -577,8 +586,9 @@ export function buildMmSubmissionData(reviewSummations = [], rawSubmissions = [] ); const scoreType = getSummationScoreClassification(summation); // Most MM review summations are provisional updates; if an entry does not - // explicitly identify itself as final, treat it as provisional. - const isProvisional = scoreType.isProvisional || !scoreType.isFinal; + // explicitly identify itself as final or example, treat it as provisional. + const isProvisional = scoreType.isProvisional + || (!scoreType.isFinal && !scoreType.isExample); const isLatest = _.isNil(summation.isLatest) ? null : Boolean(summation.isLatest); @@ -591,6 +601,7 @@ export function buildMmSubmissionData(reviewSummations = [], rawSubmissions = [] timestampValue, normalizedScore, summation, + isFinal: scoreType.isFinal, isProvisional, isLatest, },