|
| 1 | +<?php |
| 2 | + |
| 3 | +/** |
| 4 | + * Decidiq repair step — backfill labels onto NULL-label decision stages. |
| 5 | + * |
| 6 | + * Cleans up after the parafering-seam label defect: instantiate() wrote '' for |
| 7 | + * steps without labels, OpenRegister stored NULL, and the required `label` |
| 8 | + * property then 400'd the patch recording every advance — a route held over |
| 9 | + * the cross-app seam (dossiq's routes carry no step labels) wedged on its |
| 10 | + * FIRST sign-off, appending an orphan approval-action row per attempt. |
| 11 | + * |
| 12 | + * The backfill itself lives in |
| 13 | + * {@see \OCA\Decidiq\Service\DecisionStageLabelRepair}, which derives every |
| 14 | + * label through the ONE shared mapper instantiate() uses, so the two cannot |
| 15 | + * drift. This step only supplies the repair context. The orphan action rows |
| 16 | + * are deliberately KEPT — they are the audit record of what the signer did; |
| 17 | + * see that class's docblock. |
| 18 | + * |
| 19 | + * Idempotent: a stage with a label is left alone, so a re-run repairs nothing. |
| 20 | + * Registered under <post-migration> only — a fresh install has no stages yet, |
| 21 | + * let alone broken ones. |
| 22 | + * |
| 23 | + * @category Migration |
| 24 | + * @package OCA\Decidiq\Migration |
| 25 | + * |
| 26 | + * @author Conduction Development Team <info@conduction.nl> |
| 27 | + * @copyright 2026 Conduction B.V. |
| 28 | + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 |
| 29 | + * |
| 30 | + * @link https://conduction.nl |
| 31 | + * |
| 32 | + * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> |
| 33 | + * SPDX-License-Identifier: EUPL-1.2 |
| 34 | + * |
| 35 | + * @spec openspec/changes/parafering-route-runtime/specs/parafering-route-runtime/spec.md |
| 36 | + */ |
| 37 | + |
| 38 | +declare(strict_types=1); |
| 39 | + |
| 40 | +namespace OCA\Decidiq\Migration; |
| 41 | + |
| 42 | +use OCA\Decidiq\Service\SettingsService; |
| 43 | +use OCP\Migration\IOutput; |
| 44 | +use OCP\Migration\IRepairStep; |
| 45 | +use Psr\Container\ContainerInterface; |
| 46 | +use Psr\Log\LoggerInterface; |
| 47 | +use Throwable; |
| 48 | + |
| 49 | +/** |
| 50 | + * Backfills derived labels onto stages stored with a NULL label. Idempotent. |
| 51 | + * |
| 52 | + * @spec openspec/changes/parafering-route-runtime/specs/parafering-route-runtime/spec.md |
| 53 | + */ |
| 54 | +class RepairDecisionStageLabels implements IRepairStep { |
| 55 | + |
| 56 | + /** |
| 57 | + * Constructor. |
| 58 | + * |
| 59 | + * The repair service is resolved LAZILY through the container, never |
| 60 | + * constructor-injected: its store takes OpenRegister's facade by type, so |
| 61 | + * injecting it here would make this step — and with it `occ upgrade` — |
| 62 | + * fatal on an instance without openregister. |
| 63 | + * |
| 64 | + * @param SettingsService $settingsService Reports whether OpenRegister is usable. |
| 65 | + * @param ContainerInterface $container Resolves the repair service and OR's ObjectService. |
| 66 | + * @param LoggerInterface $logger Records what was repaired. |
| 67 | + */ |
| 68 | + public function __construct( |
| 69 | + private readonly SettingsService $settingsService, |
| 70 | + private readonly ContainerInterface $container, |
| 71 | + private readonly LoggerInterface $logger, |
| 72 | + ) { |
| 73 | + }//end __construct() |
| 74 | + |
| 75 | + /** |
| 76 | + * Repair-step label. |
| 77 | + * |
| 78 | + * @return string The label. |
| 79 | + * |
| 80 | + * @spec exclude Trivial repair-step label accessor. |
| 81 | + */ |
| 82 | + public function getName(): string { |
| 83 | + return 'Backfill labels onto Decidiq decision stages stored without one'; |
| 84 | + }//end getName() |
| 85 | + |
| 86 | + /** |
| 87 | + * Run the backfill. |
| 88 | + * |
| 89 | + * FAIL SOFT: a repair step that throws fails the whole `occ upgrade`, so |
| 90 | + * every failure here is logged and reported, never raised. RUN AS SYSTEM: |
| 91 | + * a repair step executes with no session, so without the scope |
| 92 | + * OpenRegister refuses the patches as 'Anonymous' — same measured trap as |
| 93 | + * the sibling migrations. |
| 94 | + * |
| 95 | + * @param IOutput $output Progress reporting. |
| 96 | + * |
| 97 | + * @return void |
| 98 | + * |
| 99 | + * @spec openspec/changes/parafering-route-runtime/specs/parafering-route-runtime/spec.md |
| 100 | + */ |
| 101 | + public function run(IOutput $output): void { |
| 102 | + if ($this->settingsService->isOpenRegisterAvailable() === false) { |
| 103 | + $output->info('OpenRegister unavailable — no stages to repair.'); |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + try { |
| 108 | + $objectService = $this->container->get('OCA\OpenRegister\Service\ObjectService'); |
| 109 | + $repairService = $this->container->get('OCA\Decidiq\Service\DecisionStageLabelRepair'); |
| 110 | + } catch (Throwable $e) { |
| 111 | + $output->warning('Could not resolve the stage-label repair: ' . $e->getMessage()); |
| 112 | + return; |
| 113 | + } |
| 114 | + |
| 115 | + try { |
| 116 | + // One system scope around the whole traversal. The repair's store |
| 117 | + // resolves the SAME shared ObjectService instance through the |
| 118 | + // container alias, so the scope set here covers its patches. |
| 119 | + $repaired = $objectService->runAsSystem( |
| 120 | + static fn (): int => (int)$repairService->repair() |
| 121 | + ); |
| 122 | + $output->info(sprintf('Backfilled labels onto %d decision stage(s).', (int)$repaired)); |
| 123 | + } catch (Throwable $e) { |
| 124 | + $this->logger->warning( |
| 125 | + 'Decidiq: decision-stage label backfill skipped', |
| 126 | + ['exception' => $e->getMessage()] |
| 127 | + ); |
| 128 | + $output->warning('Decision-stage label backfill skipped: ' . $e->getMessage()); |
| 129 | + } |
| 130 | + }//end run() |
| 131 | +}//end class |
0 commit comments