diff --git a/__tests__/shared/utils/mm-review-summations.test.js b/__tests__/shared/utils/mm-review-summations.test.js index c5981beb8..b976b883c 100644 --- a/__tests__/shared/utils/mm-review-summations.test.js +++ b/__tests__/shared/utils/mm-review-summations.test.js @@ -156,6 +156,60 @@ describe('buildMmSubmissionData', () => { ]); }); + it('derives scores from review summations embedded in raw submissions', () => { + const rawSubmissions = [ + { + createdAt: '2026-07-01T00:33:51.464Z', + id: 'submission-with-embedded-summations', + isLatest: true, + memberId: '1003', + registrant: { + memberHandle: 'gamma', + memberId: '1003', + rating: 1700, + }, + reviewSummation: [ + { + aggregateScore: 68.94, + id: 'summation-example', + isExample: true, + reviewedDate: '2026-07-01T00:35:00.000Z', + submissionId: 'submission-with-embedded-summations', + }, + { + aggregateScore: 75.82, + id: 'summation-provisional', + isProvisional: true, + reviewedDate: '2026-07-01T00:36:00.000Z', + submissionId: 'submission-with-embedded-summations', + }, + { + aggregateScore: 75.87, + id: 'summation-final', + isFinal: true, + reviewedDate: '2026-07-01T00:37:00.000Z', + submissionId: 'submission-with-embedded-summations', + }, + ], + status: 'completed', + }, + ]; + + const result = buildMmSubmissionData([], rawSubmissions); + + expect(result).toHaveLength(1); + expect(result[0].submissions).toEqual([ + expect.objectContaining({ + finalScore: 75.87, + provisionalScore: 75.82, + reviewSummations: expect.arrayContaining([ + expect.objectContaining({ id: 'summation-example' }), + ]), + submissionId: 'submission-with-embedded-summations', + }), + ]); + }); + it('uses v6 submitter fields and submittedDate for imported raw submissions', () => { const rawSubmissions = [ { diff --git a/src/shared/utils/mm-review-summations.js b/src/shared/utils/mm-review-summations.js index 671fc48e2..d0fdcdd95 100644 --- a/src/shared/utils/mm-review-summations.js +++ b/src/shared/utils/mm-review-summations.js @@ -398,6 +398,59 @@ function updateSubmissionEntry( }; } +/** + * Applies a review summation to a normalized Marathon Match submission entry. + * + * The Review API can return summations either as top-level + * /reviewSummations rows or embedded in /submissions reviewSummation arrays. + * This helper keeps both shapes on the same score normalization path. + * + * @param {Object} existingEntry Existing normalized submission entry. + * @param {Object} options Summation merge options. + * @param {String} options.submissionId Submission id that owns the summation. + * @param {Object} options.summation Review summation returned by the API. + * @param {String|null} options.fallbackTimestamp Timestamp to use if the + * summation does not include one. + * @param {Boolean|null} options.isLatest Latest-submission flag from the + * owning submission or summation, when available. + * @return {Object} Submission entry with score fields updated from summation. + * @throws This function does not throw. + */ +function updateSubmissionEntryFromReviewSummation( + existingEntry, + { + submissionId, + summation, + fallbackTimestamp = null, + isLatest = null, + }, +) { + const timestamp = getSummationTimestamp(summation) || fallbackTimestamp; + const timestampValue = toTimestampValue(timestamp); + const normalizedScore = normalizeScoreValue( + _.get(summation, 'aggregateScore'), + ); + const scoreType = getSummationScoreClassification(summation); + // Most MM review summations are provisional updates; if an entry does not + // explicitly identify itself as final or example, treat it as provisional. + const isProvisional = scoreType.isProvisional + || (!scoreType.isFinal && !scoreType.isExample); + + return updateSubmissionEntry( + existingEntry, + { + submissionId, + timestamp, + timestampValue, + normalizedScore, + summation, + isFinal: scoreType.isFinal, + isProvisional, + isLatest, + }, + ); +} + function updateSubmissionEntryFromSubmission( existingEntry, { @@ -663,31 +716,15 @@ export function buildMmSubmissionData(reviewSummations = [], rawSubmissions = [] const submissionId = rawSubmissionId ? _.toString(rawSubmissionId) : `unknown-${handle}-${index}`; - const timestamp = getSummationTimestamp(summation); - const timestampValue = toTimestampValue(timestamp); - - const normalizedScore = normalizeScoreValue( - _.get(summation, 'aggregateScore'), - ); - const scoreType = getSummationScoreClassification(summation); - // Most MM review summations are provisional updates; if an entry does not - // 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); - const updatedEntry = updateSubmissionEntry( + const updatedEntry = updateSubmissionEntryFromReviewSummation( memberEntry.submissionsMap.get(submissionId), { submissionId, - timestamp, - timestampValue, - normalizedScore, summation, - isFinal: scoreType.isFinal, - isProvisional, isLatest, }, ); @@ -746,8 +783,21 @@ export function buildMmSubmissionData(reviewSummations = [], rawSubmissions = [] : []), ]); - const updatedEntry = updateSubmissionEntryFromSubmission( + const entryWithEmbeddedSummations = reviewSummation.reduce( + (entry, summation) => updateSubmissionEntryFromReviewSummation( + entry, + { + submissionId, + summation, + fallbackTimestamp: timestamp, + isLatest, + }, + ), memberEntry.submissionsMap.get(submissionId), + ); + + const updatedEntry = updateSubmissionEntryFromSubmission( + entryWithEmbeddedSummations, { submissionId, timestamp,