From e03f68a86a96d3fd89676664dc3761e85cc29422 Mon Sep 17 00:00:00 2001 From: Touhidur Rahman Date: Tue, 28 Apr 2026 17:55:47 +0600 Subject: [PATCH] pkp/pkp-lib#12140 updated migration for 8->3 collision --- .../I11241_MissingDecisionConstantsUpdate.php | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/classes/migration/upgrade/v3_5_0/I11241_MissingDecisionConstantsUpdate.php b/classes/migration/upgrade/v3_5_0/I11241_MissingDecisionConstantsUpdate.php index 29b779ef7bf..4c1c8a2bf12 100644 --- a/classes/migration/upgrade/v3_5_0/I11241_MissingDecisionConstantsUpdate.php +++ b/classes/migration/upgrade/v3_5_0/I11241_MissingDecisionConstantsUpdate.php @@ -38,13 +38,6 @@ public function getDecisionMappings(): array 'updated_value' => 2, ], - // \PKP\decision\Decision::EXTERNAL_REVIEW - [ - 'stage_id' => [WORKFLOW_STAGE_ID_SUBMISSION], - 'current_value' => 8, - 'updated_value' => 3, - ], - // \PKP\decision\Decision::SKIP_EXTERNAL_REVIEW [ 'stage_id' => [WORKFLOW_STAGE_ID_SUBMISSION], @@ -133,7 +126,63 @@ public function up(): void 'updated_at' => Carbon::now(), ]) ); - + + // \PKP\decision\Decision::EXTERNAL_REVIEW (8→3) — special handling + // + // After the buggy I7725 ran, both stranded EXTERNAL_REVIEW rows and + // correctly-migrated INITIAL_DECLINE rows share decision=8, stage_id=1. + // They are indistinguishable from edit_decisions alone. + // + // Two-layer disambiguation: + // 1. whereExists(review_rounds at EXTERNAL_REVIEW stage) — the submission + // must have actually reached external review + // 2. whereNotExists(later decision=8 at same submission/stage) — only + // the MOST RECENT decision=8 per submission is the EXTERNAL_REVIEW; + // all earlier ones were INITIAL_DECLINE + // + // Why "most recent" instead of checking for REVERT_INITIAL_DECLINE: + // OJS 3.3 had a loose workflow that allowed editors to decline a submission + // and then send it to review WITHOUT recording a REVERT_INITIAL_DECLINE. + // The "most recent" check handles ALL cases: with revert, without revert, + // and multiple decline cycles. + + // Collect the IDs of the MOST RECENT decision=8 per submission + // (the EXTERNAL_REVIEW row). Done as a separate SELECT because MySQL + // does not allow referencing the target table in an UPDATE subquery. + $externalReviewIds = DB::table('edit_decisions') + ->where('edit_decisions.stage_id', WORKFLOW_STAGE_ID_SUBMISSION) + ->where('edit_decisions.decision', 8) + ->whereNull('edit_decisions.updated_at') + ->where('edit_decisions.date_decided', '<', $firstVersionOf34->date_installed) + ->whereExists(function ($query) { + $query->select(DB::raw(1)) + ->from('review_rounds') + ->whereColumn('review_rounds.submission_id', 'edit_decisions.submission_id') + ->where('review_rounds.stage_id', WORKFLOW_STAGE_ID_EXTERNAL_REVIEW); + }) + ->whereNotExists(function ($query) use ($firstVersionOf34) { + // If a LATER decision=8 at stage_id=1 exists for the same submission, + // then this row was an earlier INITIAL_DECLINE — not the final + // EXTERNAL_REVIEW that triggered the review + $query->select(DB::raw(1)) + ->from('edit_decisions as later_ed') + ->whereColumn('later_ed.submission_id', 'edit_decisions.submission_id') + ->where('later_ed.decision', 8) + ->where('later_ed.stage_id', WORKFLOW_STAGE_ID_SUBMISSION) + ->whereColumn('later_ed.date_decided', '>', 'edit_decisions.date_decided') + ->where('later_ed.date_decided', '<', $firstVersionOf34->date_installed); + }) + ->pluck('edit_decision_id'); + + if ($externalReviewIds->isNotEmpty()) { + DB::table('edit_decisions') + ->whereIn('edit_decision_id', $externalReviewIds) + ->update([ + 'decision' => 3, + 'updated_at' => Carbon::now(), + ]); + } + $this->removeUpdatedAtColumn(); }