@@ -46,51 +46,6 @@ function isDesignTrack(track) {
4646 return normalizeTrackToken ( track ) === DESIGN_TRACK ;
4747}
4848
49- /**
50- * Resolve the requested scheduled start date from a phase update payload.
51- *
52- * @param {Object } phase existing challenge phase
53- * @param {Object|null|undefined } newPhase phase update payload
54- * @returns {Date|String|undefined } requested scheduled start date, falling back to current start
55- */
56- function resolveRequestedScheduledStartDate ( phase , newPhase ) {
57- if ( _ . isNil ( newPhase ) || _ . isNil ( _ . get ( newPhase , "scheduledStartDate" ) ) ) {
58- return _ . get ( phase , "scheduledStartDate" ) ;
59- }
60-
61- return _ . get ( newPhase , "scheduledStartDate" ) ;
62- }
63-
64- /**
65- * Resolve the requested scheduled end date from a phase update payload.
66- *
67- * @param {Object } phase existing challenge phase
68- * @param {Object|null|undefined } newPhase phase update payload
69- * @returns {Date|String|undefined } requested scheduled end date when the payload changes it
70- */
71- function resolveRequestedScheduledEndDate ( phase , newPhase ) {
72- if ( _ . isNil ( newPhase ) ) {
73- return undefined ;
74- }
75-
76- if ( ! _ . isNil ( _ . get ( newPhase , "scheduledEndDate" ) ) ) {
77- return _ . get ( newPhase , "scheduledEndDate" ) ;
78- }
79-
80- const requestedDuration = _ . get ( newPhase , "duration" ) ;
81- const requestedScheduledStartDate = resolveRequestedScheduledStartDate ( phase , newPhase ) ;
82- if ( _ . isNil ( requestedDuration ) || _ . isNil ( requestedScheduledStartDate ) ) {
83- return undefined ;
84- }
85-
86- const scheduledStart = moment ( requestedScheduledStartDate ) ;
87- if ( ! scheduledStart . isValid ( ) ) {
88- return undefined ;
89- }
90-
91- return scheduledStart . add ( requestedDuration , "seconds" ) . toDate ( ) . toISOString ( ) ;
92- }
93-
9449/**
9550 * Check whether a requested schedule reduces the phase duration.
9651 *
@@ -187,6 +142,58 @@ function validateActivePhaseScheduledEndDateChange(
187142 }
188143}
189144
145+ /**
146+ * Validate recalculated schedules against persisted phase schedules.
147+ *
148+ * @param {Array<Object> } originalPhases persisted challenge phases before the update
149+ * @param {Array<Object> } updatedPhases recalculated challenge phases that will be persisted
150+ * @param {Object } options validation options forwarded to the phase schedule validator
151+ * @returns {undefined } validates only
152+ * @throws {BadRequestError } when a recalculated schedule violates shortening rules
153+ */
154+ function validateRecalculatedPhaseSchedules ( originalPhases , updatedPhases , options = { } ) {
155+ const originalById = new Map ( ) ;
156+ const originalByPhaseId = new Map ( ) ;
157+
158+ _ . each ( originalPhases , ( phase ) => {
159+ if ( _ . isNil ( phase ) ) {
160+ return ;
161+ }
162+
163+ if ( ! _ . isNil ( phase . id ) ) {
164+ originalById . set ( phase . id , phase ) ;
165+ }
166+ if ( ! _ . isNil ( phase . phaseId ) ) {
167+ originalByPhaseId . set ( phase . phaseId , phase ) ;
168+ }
169+ } ) ;
170+
171+ _ . each ( updatedPhases , ( updatedPhase ) => {
172+ if ( _ . isNil ( updatedPhase ) ) {
173+ return ;
174+ }
175+
176+ const originalPhase = (
177+ ! _ . isNil ( updatedPhase . id )
178+ ? originalById . get ( updatedPhase . id )
179+ : undefined
180+ ) || originalByPhaseId . get ( updatedPhase . phaseId ) ;
181+
182+ if ( _ . isNil ( originalPhase ) ) {
183+ return ;
184+ }
185+
186+ validateActivePhaseScheduledEndDateChange (
187+ originalPhase ,
188+ updatedPhase . scheduledEndDate ,
189+ {
190+ ...options ,
191+ requestedScheduledStartDate : updatedPhase . scheduledStartDate ,
192+ }
193+ ) ;
194+ } ) ;
195+ }
196+
190197/**
191198 * Apply an explicit scheduled end date to a phase and update its duration.
192199 *
@@ -384,14 +391,6 @@ class ChallengePhaseHelper {
384391 const phaseFromTemplate = timelineTemplateMap . get ( phase . phaseId ) ;
385392 const phaseDefinition = phaseDefinitionMap . get ( phase . phaseId ) ;
386393 const newPhase = findPhaseUpdate ( newPhases , phase ) ;
387- validateActivePhaseScheduledEndDateChange (
388- phase ,
389- resolveRequestedScheduledEndDate ( phase , newPhase ) ,
390- {
391- ...options ,
392- requestedScheduledStartDate : resolveRequestedScheduledStartDate ( phase , newPhase ) ,
393- }
394- ) ;
395394 const templatePredecessor = _ . get ( phaseFromTemplate , "predecessor" ) ;
396395 // Prefer template predecessor only when that phase exists on the challenge, otherwise keep the stored link.
397396 const resolvedPredecessor = _ . isNil ( phaseFromTemplate )
@@ -485,6 +484,7 @@ class ChallengePhaseHelper {
485484 }
486485 recalculateScheduledEndDate ( phase ) ;
487486 }
487+ validateRecalculatedPhaseSchedules ( challengePhasesOrdered , updatedPhases , options ) ;
488488 return _ . map ( updatedPhases , ( phase ) => _ . omit ( phase , "requestedScheduledEndDate" ) ) ;
489489 }
490490
0 commit comments