From de5f836b964e73b88f27f5f97661cbc58d8f5d30 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 19 Nov 2025 20:50:50 +0200 Subject: [PATCH] HistoricPetitionViewer and Transmogrification adjucstments --- .../HistoricPetitionViewerPlugin.php | 8 +- .../HistoricPetitionViewer/config/plugin.json | 25 ++-- .../HistoricPetitionViewersController.php | 40 ++++++ ...tepLink.php => HistoricPetitionViewer.php} | 4 +- .../Table/HistoricPetitionAttributesTable.php | 13 +- .../HistoricPetitionMetadataRecordsTable.php | 7 +- ...e.php => HistoricPetitionViewersTable.php} | 11 +- ...ll.php => HistoricPetitionViewersCell.php} | 40 ++++-- .../HistoricPetitionViewers/fields.inc | 32 +++++ .../HistoricPetitionStepLinks/display.php | 104 -------------- .../cell/HistoricPetitionViewers/display.php | 133 ++++++++++++++++++ .../Transmogrify/config/schema/tables.json | 6 +- .../src/Lib/Traits/CacheTrait.php | 39 ++++- .../src/Lib/Traits/RowTransformationTrait.php | 91 +++++++++++- .../src/Lib/Traits/TypeMapperTrait.php | 30 ++++ 15 files changed, 432 insertions(+), 151 deletions(-) create mode 100644 app/plugins/HistoricPetitionViewer/src/Controller/HistoricPetitionViewersController.php rename app/plugins/HistoricPetitionViewer/src/Model/Entity/{HistoricPetitionStepLink.php => HistoricPetitionViewer.php} (93%) rename app/plugins/HistoricPetitionViewer/src/Model/Table/{HistoricPetitionStepLinksTable.php => HistoricPetitionViewersTable.php} (93%) rename app/plugins/HistoricPetitionViewer/src/View/Cell/{HistoricPetitionStepLinksCell.php => HistoricPetitionViewersCell.php} (65%) create mode 100644 app/plugins/HistoricPetitionViewer/templates/HistoricPetitionViewers/fields.inc delete mode 100644 app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php create mode 100644 app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionViewers/display.php diff --git a/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php b/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php index 714f1692b..1355e0fd8 100644 --- a/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php +++ b/app/plugins/HistoricPetitionViewer/HistoricPetitionViewerPlugin.php @@ -40,12 +40,14 @@ public function bootstrap(PluginApplicationInterface $app): void public function routes(RouteBuilder $routes): void { $routes->plugin( - 'HistoryPetitionViewer', + 'HistoricPetitionViewer', ['path' => '/historic-petition-viewer'], function (RouteBuilder $builder) { - // Add custom routes here + // Your snippet goes here, using $builder instead of $routes + $builder->setRouteClass(DashedRoute::class); - $builder->fallbacks(); + // Add your plugin routes here if needed, then: + $builder->fallbacks(DashedRoute::class); } ); parent::routes($routes); diff --git a/app/plugins/HistoricPetitionViewer/config/plugin.json b/app/plugins/HistoricPetitionViewer/config/plugin.json index 3cd8921a3..01fde4d09 100644 --- a/app/plugins/HistoricPetitionViewer/config/plugin.json +++ b/app/plugins/HistoricPetitionViewer/config/plugin.json @@ -10,11 +10,17 @@ "columns": { "id": { "type": "integer", "autoincrement": true, "primarykey": true }, "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, + "historic_petition_viewer_id": { + "type": "integer", + "foreignkey": { "table": "historic_petition_viewers", "column": "id" }, + "notnull": false + }, + "attribute": { "type": "string", "size": 128 }, "value": { "type": "text" }, "created": { "type": "datetime" }, "modified": { "type": "datetime" }, - "historic_petition_attribute_id": { "type": "integer", "foreignkey": { "table": "petition_hist_attrs", "column": "id" } }, + "petition_hist_attr_id": { "type": "integer", "foreignkey": { "table": "petition_hist_attrs", "column": "id" } }, "revision": { "type": "integer" }, "deleted": { "type": "boolean" }, "actor_identifier": { "type": "string", "size": 256 } @@ -22,7 +28,7 @@ "indexes": { "petition_hist_attrs_i1": { "columns": [ "petition_id" ] }, "petition_hist_attrs_i2": { "columns": [ "attribute" ] }, - "petition_hist_attrs_i3": { "columns": [ "historic_petition_attribute_id" ] } + "petition_hist_attrs_i3": { "columns": [ "petition_hist_attr_id" ] } }, "timestamps": false, "changelog": false @@ -32,6 +38,12 @@ "columns": { "id": { "type": "integer", "autoincrement": true, "primarykey": true }, "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, + "enrollment_flow_id": { "type": "integer", "foreignkey": { "table": "enrollment_flows", "column": "id" }}, + "historic_petition_viewer_id": { + "type": "integer", + "foreignkey": { "table": "historic_petition_viewers", "column": "id" }, + "notnull": false + }, "enrollee_org_identity_id": { "type": "integer" }, "archived_org_identity_id": { "type": "integer" }, @@ -60,17 +72,14 @@ "timestamps": false, "changelog": false }, - "historic_petition_step_links": { - "comment": "Associates a Petition’s historic data to a specific Enrollment Flow Step for read-only viewing", + "historic_petition_viewers": { + "comment": "Enrollment Flow Step for historic petition data", "columns": { "id": { "type": "integer", "autoincrement": true, "primarykey": true }, - "petition_id": { "type": "integer", "foreignkey": { "table": "petitions", "column": "id" } }, "enrollment_flow_step_id": { "type": "integer", "foreignkey": { "table": "enrollment_flow_steps", "column": "id" } } }, "indexes": { - "historic_petition_step_links_i1": { "columns": [ "petition_id" ] }, - "historic_petition_step_links_i2": { "columns": [ "enrollment_flow_step_id" ] }, - "historic_petition_step_links_u1": { "columns": [ "petition_id", "enrollment_flow_step_id" ] } + "historic_petition_step_links_i1": { "columns": [ "enrollment_flow_step_id" ] } } } } diff --git a/app/plugins/HistoricPetitionViewer/src/Controller/HistoricPetitionViewersController.php b/app/plugins/HistoricPetitionViewer/src/Controller/HistoricPetitionViewersController.php new file mode 100644 index 000000000..0f4b5a0c0 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/src/Controller/HistoricPetitionViewersController.php @@ -0,0 +1,40 @@ + [ + 'HistoricPetitionViewers.id' => 'asc' + ] + ]; +} diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionViewer.php similarity index 93% rename from app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php rename to app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionViewer.php index 0788334ec..8ada2824e 100644 --- a/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionStepLink.php +++ b/app/plugins/HistoricPetitionViewer/src/Model/Entity/HistoricPetitionViewer.php @@ -1,6 +1,6 @@ setTable('petition_hist_attrs'); + $this->setPrimaryKey('id'); + $this->setDisplayField('id'); $this->addBehavior('Changelog'); $this->addBehavior('Log'); @@ -56,12 +60,13 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('Petitions'); + $this->belongsTo('HistoricPetitionViewers'); $this->setDisplayField('attribute'); $this->setPrimaryLink('petition_id'); $this->setRequiresCO(false); -// $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + $this->setAllowLookupPrimaryLink(['display']); $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) @@ -106,11 +111,11 @@ public function validationDefault(Validator $validator): Validator { // value (text, optional) $validator->allowEmptyString('value'); - // historic_petition_attribute_id (self-referential changelog FK, optional) - $validator->add('historic_petition_attribute_id', [ + // petition_hist_attr_id (self-referential changelog FK, optional) + $validator->add('petition_hist_attr_id', [ 'content' => ['rule' => 'isInteger'] ]); - $validator->allowEmptyString('historic_petition_attribute_id'); + $validator->allowEmptyString('petition_hist_attr_id'); // revision (optional integer) $validator->add('revision', [ diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadataRecordsTable.php b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadataRecordsTable.php index 89a6da237..99a632564 100644 --- a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadataRecordsTable.php +++ b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionMetadataRecordsTable.php @@ -34,6 +34,7 @@ use Cake\Validation\Validator; class HistoricPetitionMetadataRecordsTable extends Table { + use \App\Lib\Traits\CoLinkTrait; use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\LayoutTrait; use \App\Lib\Traits\PermissionsTrait; @@ -47,6 +48,8 @@ public function initialize(array $config): void { // Map model to shorter physical table name $this->setTable('petition_meta_hist_recs'); + $this->setPrimaryKey('id'); + $this->setDisplayField('id'); $this->addBehavior('Changelog'); $this->addBehavior('Log'); @@ -56,11 +59,13 @@ public function initialize(array $config): void { // Define associations $this->belongsTo('Petitions'); + $this->belongsTo('HistoricPetitionViewers'); + $this->belongsTo('EnrollmentFlows'); $this->setDisplayField('petition_id'); $this->setRequiresCO(false); - $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + $this->setAllowLookupPrimaryLink(['display']); $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) diff --git a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionViewersTable.php similarity index 93% rename from app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php rename to app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionViewersTable.php index 1fda42bc9..221f930f5 100644 --- a/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionStepLinksTable.php +++ b/app/plugins/HistoricPetitionViewer/src/Model/Table/HistoricPetitionViewersTable.php @@ -33,7 +33,8 @@ use Cake\ORM\Table; use Cake\Validation\Validator; -class HistoricPetitionStepLinksTable extends Table { +class HistoricPetitionViewersTable extends Table { + use \App\Lib\Traits\CoLinkTrait; use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\LayoutTrait; use \App\Lib\Traits\PermissionsTrait; @@ -59,20 +60,20 @@ public function initialize(array $config): void { $this->setPrimaryLink('enrollment_flow_step_id'); $this->setRequiresCO(false); - $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + $this->setAllowLookupPrimaryLink(['display']); // All the tabs share the same configuration in the ModelTable file $this->setTabsConfig( [ // Ordered list of Tabs - 'tabs' => ['EnrollmentFlowSteps', 'HistoricPetitionViewer.HistoricPetitionStepLinks'], + 'tabs' => ['EnrollmentFlowSteps', 'HistoricPetitionViewer.HistoricPetitionViewers'], // What actions will include the subnavigation header 'action' => [ // If a model renders in a subnavigation mode in edit/view mode, it cannot // render in index mode for the same use case/context // XXX edit should go first. 'EnrollmentFlowSteps' => ['view'], - 'HistoricPetitionViewer.HistoricPetitionStepLinks' => ['view'] + 'HistoricPetitionViewer.HistoricPetitionViewers' => ['view'] ] ] ); @@ -83,7 +84,7 @@ public function initialize(array $config): void { 'delete' => false, 'dispatch' => false, 'display' => true, - 'edit' => false, + 'edit' => true, 'view' => ['platformAdmin', 'coAdmin'] ], // Actions that operate over a table (ie: do not require an $id) diff --git a/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php b/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionViewersCell.php similarity index 65% rename from app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php rename to app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionViewersCell.php index e23455189..a5f29cec7 100644 --- a/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionStepLinksCell.php +++ b/app/plugins/HistoricPetitionViewer/src/View/Cell/HistoricPetitionViewersCell.php @@ -31,14 +31,11 @@ declare(strict_types=1); -namespace CoreEnroller\View\Cell; +namespace HistoricPetitionViewer\View\Cell; use Cake\View\Cell; -/** - * BasicAttributeCollectors cell - */ -class HistoricPetitionStepLinksCell extends Cell +class HistoricPetitionViewersCell extends Cell { /** * @var mixed @@ -85,21 +82,38 @@ public function initialize(): void */ public function display(int $petitionId): void { - // Fetch historic metadata rows for this petition (could be zero or more) - $historicMetadataRecords = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionMetadataRecords') - ->find() + // Latest (current) metadata record for this petition + $metaTable = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionMetadataRecords'); + $latestMeta = $metaTable->find() + ->applyOptions(['archived' => false]) ->where(['petition_id' => $petitionId]) + ->orderBy(['id' => 'DESC']) + ->limit(1) ->all(); - // Fetch historic attributes rows for this petition (could be zero or more) - $historicAttributes = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionAttributes') - ->find() + // Current attributes for this petition, deduplicated to latest row per attribute + $attrTable = $this->fetchTable('HistoricPetitionViewer.HistoricPetitionAttributes'); + + // Subquery: max(id) per attribute for the petition + $sub = $attrTable->find() + ->select([ + 'max_id' => $attrTable->find()->func()->max('id'), + ]) + ->applyOptions(['archived' => false]) ->where(['petition_id' => $petitionId]) + ->group('attribute'); + + $latestAttrs = $attrTable->find() + ->applyOptions(['archived' => false]) + ->where(function ($exp) use ($sub) { + // id IN (SELECT max(id) ...) for the petition, one per attribute + return $exp->in('HistoricPetitionAttributes.id', $sub); + }) ->orderBy(['attribute' => 'ASC', 'id' => 'ASC']) ->all(); - $this->set('vv_historic_petition_metadata_records', $historicMetadataRecords); - $this->set('vv_historic_petition_attributes', $historicAttributes); + $this->set('vv_historic_petition_metadata_records', $latestMeta); // array with at most 1 record + $this->set('vv_historic_petition_attributes', $latestAttrs); // deduped by attribute $this->set('vv_obj', $this->vv_obj); } } diff --git a/app/plugins/HistoricPetitionViewer/templates/HistoricPetitionViewers/fields.inc b/app/plugins/HistoricPetitionViewer/templates/HistoricPetitionViewers/fields.inc new file mode 100644 index 000000000..20843df67 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/templates/HistoricPetitionViewers/fields.inc @@ -0,0 +1,32 @@ +Field->disableFormEditMode(); + +// There are currently no configurable options for the SSH Key Authenticator +print $this->element('notify/banner', ['info' => __d('information', 'plugin.config.none')]); \ No newline at end of file diff --git a/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php b/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php deleted file mode 100644 index 4055cd219..000000000 --- a/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionStepLinks/display.php +++ /dev/null @@ -1,104 +0,0 @@ -id === null) { - echo __d('error', 'notfound', 'Historic Petition'); - return; -} - -// Normalize collections -$metaRows = $vv_historic_petition_metadata_records ?? []; -$attrRows = $vv_historic_petition_attributes ?? []; - -// Helper: render a KV list from an entity/array, excluding technical fields -$excludeMetaKeys = [ - 'id', 'petition_id', - 'historic_petition_metadata_id', 'revision', 'deleted', - 'actor_identifier', 'created', 'modified' -]; - -// Order token-like fields first for readability if present -$preferredMetaOrder = [ - 'approver_comment', - 'return_url', - 'token', 'petitioner_token', 'enrollee_token', - 'enrollee_org_identity_id', 'archived_org_identity_id', 'enrollee_person_role_id', - 'sponsor_person_id', 'approver_person_id', - 'co_invite_id', 'vetting_request_id', -]; - -function renderMetaKeyValue(string $label, mixed $value): void { - if ($value === null || $value === '') { - return; - } - ?> -
  • -
    -
    -
  • - -
    -

    #id) ?>

    - - - -
    -

    - - -
      - - attribute ?? null) : ($row['attribute'] ?? null); - $value = is_object($row) ? ($row->value ?? null) : ($row['value'] ?? null); - ?> - -
    • -
      -
      -
    • - -
    - -

    - -
    -
    diff --git a/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionViewers/display.php b/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionViewers/display.php new file mode 100644 index 000000000..570801358 --- /dev/null +++ b/app/plugins/HistoricPetitionViewer/templates/cell/HistoricPetitionViewers/display.php @@ -0,0 +1,133 @@ +id)) ? (string)$vv_obj->id : null; + +if ($entityId === null) { + echo __d('error', 'notfound', 'Historic Petition'); + return; +} + +// Helpers +$toArr = static function ($row): array { + if (is_array($row)) return $row; + if (is_object($row)) return method_exists($row, 'toArray') ? (array)$row->toArray() : (array)$row; + return []; +}; + +$prettify = static function (string $label): string { + // petition_meta_hist_rec_id -> Petition Meta Hist Rec Id + $label = str_replace(['_', '-'], ' ', strtolower($label)); + $label = preg_replace('/\s+/', ' ', trim($label)); + return ucwords($label); +}; + +// Do not alter font coloring; keep defaults +// Each row: label 1/3, value 2/3; both left-aligned; 1em padding inside each cell. +$renderLi = static function (string $label, $value) { + if ($value === null || $value === '') return; + $out = is_scalar($value) ? (string)$value : json_encode($value); + ?> +
  • +
    +
    + +
    +
    + +
    +
    +
  • + +
    +

    #

    + + + +
    + +
    + + + + + + +

    + +
    + + +
    + +
      + + + +
    + +

    + +
    +
    +
    diff --git a/app/plugins/Transmogrify/config/schema/tables.json b/app/plugins/Transmogrify/config/schema/tables.json index 6edfb35d2..c3dfca2e7 100644 --- a/app/plugins/Transmogrify/config/schema/tables.json +++ b/app/plugins/Transmogrify/config/schema/tables.json @@ -440,7 +440,7 @@ "collect_enrollee_email" ], "cache": ["co_id", "auth_cou_id", "authz_co_group_id"], - "postRow": null, + "postRow": "createEnrollmentFlowStep", "fieldMap": { "authz_level": "authz_type", "authz_co_group_id": "authz_group_id", @@ -521,9 +521,10 @@ "source": "cm_co_petitions", "displayField": "id", "fieldMap": { - "co_enrollment_flow_id": null, + "co_enrollment_flow_id": "enrollment_flow_id", "co_id": null, "cou_id": null, + "historic_petition_viewer_id" :"&mapHistoricPetitionViewerId", "enrollee_org_identity_id": "enrollee_org_identity_id", "archived_org_identity_id": "archived_org_identity_id", "enrollee_co_person_id": null, @@ -542,6 +543,7 @@ "displayField": "id", "fieldMap": { "co_petition_id": "petition_id", + "historic_petition_viewer_id" :"&mapHistoricPetitionViewerId", "co_enrollment_attribute_id": null, "attribute_foreign_key": null, "co_petition_attribute_id": null diff --git a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php index c5b55c05f..9870abab7 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php @@ -53,14 +53,14 @@ protected function cacheResults(string $table, array $row): void } // Cache the requested fields. For now, at least, we key on row ID only. - foreach($this->tables[$table]['cache'] as $field) { - if(is_array($field)) { + foreach ($this->tables[$table]['cache'] as $field) { + if (is_array($field)) { // This is a list of fields, create a composite key that point to the row ID $label = ""; $key = ""; - foreach($field as $subfield) { + foreach ($field as $subfield) { // eg: co_id+attribute+value+ $label .= $subfield . "+"; @@ -68,11 +68,38 @@ protected function cacheResults(string $table, array $row): void $key .= $row[$subfield] . "+"; } + $this->cache[$table][$label] ??= []; $this->cache[$table][$label][$key] = $row['id']; } else { - // If the row has the field then map id to the requested field. - if(array_key_exists($field, $row)) { - $this->cache[$table]['id'][ $row['id'] ][$field] = $row[$field]; + // If the row has the field then map id to the requested field safely. + if (array_key_exists($field, $row)) { + $id = $row['id'] ?? null; + if ($id === null) { + continue; + } + + // Ensure the id bucket is initialized + $this->cache[$table]['id'] ??= []; + $this->cache[$table]['id'][$id] ??= []; + + // If a value exists: + // - If both existing and incoming are arrays, merge recursively. + // - Otherwise, set only when the key is not present to avoid clobbering + // previously injected/nested data (like `enrollment_flow_steps`). + if (array_key_exists($field, $this->cache[$table]['id'][$id])) { + $existing = $this->cache[$table]['id'][$id][$field]; + $incoming = $row[$field]; + + if (is_array($existing) && is_array($incoming)) { + $this->cache[$table]['id'][$id][$field] = array_replace_recursive($existing, $incoming); + } else { + // Do not overwrite an existing non-array value or structure + // If you do want to force an overwrite for specific fields, handle by name here. + // e.g., if (in_array($field, ['co_id', ...], true)) { $this->cache[...] = $incoming; } + } + } else { + $this->cache[$table]['id'][$id][$field] = $row[$field]; + } } } } diff --git a/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php index 5aae0fe2b..2c4f59626 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php @@ -36,6 +36,7 @@ trait RowTransformationTrait { + use CacheTrait; /** * Apply validation rules for CoGroup names during transformation @@ -316,6 +317,85 @@ protected function migrateExtendedAttributesToAdHocAttributes(): void } } + /** + * Creates a new enrollment flow step for the given enrollment flow + * + * @param array $origRow Original row data from source database + * @param array $row Current row data for target database + * @return void + * @throws Exception When database operation fails + * @since COmanage Registry v5.2.0 + */ + protected function createEnrollmentFlowStep(array $origRow, array $row): void { + // Only for non-deleted, non-revision rows + if (!empty($origRow['deleted'])) { + return; + } + if (!empty($origRow['co_enrollment_flow_id'])) { + return; + } + + // We need the target Enrollment Flow ID (assumes ID is preserved/mapped on insert) + if (empty($row['id'])) { + // If the target ID is not present, we can't safely create the step + return; + } + $flowId = (int)$row['id']; + + // Qualified table names + $stepsTable = $this->outconn->qualifyTableName('enrollment_flow_steps'); + $viewerTable = $this->outconn->qualifyTableName('historic_petition_viewers'); + + // Build step row (bypass ORM behaviors -> set timestamps + changelog fields) + $step = [ + 'enrollment_flow_id' => $flowId, + 'description' => 'Petition Historic Data', + 'status' => 'A', // Active + 'actor_type' => 'E', // Enrollee + 'plugin' => 'HistoricPetitionViewer.HistoricPetitionViewers', + 'ordr' => 1, + 'created' => $this->mapNow([]), + 'modified' => $this->mapNow([]), + ]; + $this->populateChangelogDefaults('enrollment_flow_steps', $step, true); + $this->normalizeBooleanFieldsForDb('enrollment_flow_steps', $step); + + $this->outconn->beginTransaction(); + try { + // Insert Enrollment Flow Step + $this->outconn->insert($stepsTable, $step); + + if (!method_exists($this->outconn, 'lastInsertId')) { + throw new \RuntimeException('Could not retrieve Enrollment Flow Step ID'); + } + $stepId = (int)$this->outconn->lastInsertId(); + + // Create the Historic Petition Viewer row pointing to the step + $viewer = [ + 'enrollment_flow_step_id' => $stepId, + 'created' => $this->mapNow([]), + 'modified' => $this->mapNow([]), + ]; + $this->populateChangelogDefaults('historic_petition_viewers', $viewer, true); + $this->normalizeBooleanFieldsForDb('historic_petition_viewers', $viewer); + + $this->outconn->insert($viewerTable, $viewer); + + // Retrieve viewer ID + $viewerId = (int)$this->outconn->lastInsertId(); + + $this->outconn->commit(); + + // Cache both IDs under enrollment_flows + $this->cache['enrollment_flows']['id'][$flowId]['enrollment_flow_steps']['id'] = $stepId; + $this->cache['enrollment_flows']['id'][$flowId]['historic_petition_viewers']['id'] = $viewerId; + } catch (\Throwable $e) { + $this->outconn->rollBack(); + throw new \RuntimeException("Failed to create enrollment flow step for enrollment flow $flowId: " . $e->getMessage()); + } + } + + /** * Split an External Identity into an External Identity Role. * @@ -421,7 +501,7 @@ private function performNoMapping(array &$row, string $oldname): void * @param string $funcName Name of mapping function to call * @param string $table Table name for error reporting * @return void - * @throws \InvalidArgumentException When mapping returns falsy value or function not found + * @throws \InvalidArgumentException When mapping returns the falsy value or function not found */ private function performFunctionMapping(array &$row, string $oldname, string $funcName, string $table): void { @@ -432,8 +512,13 @@ private function performFunctionMapping(array &$row, string $oldname, string $fu // We always pass the entire row so the mapping function can implement arbitrary logic $row[$oldname] = $this->$funcName($row); - // NOTE: mapAffiliationType can return null since extended types might not exist for deleted COs - if (!$row[$oldname] && $funcName !== 'mapAffiliationType') { + // Allow specific mapping functions to return null (target columns are nullable) + $nullableFuncs = [ + 'mapAffiliationType', + 'mapHistoricPetitionViewerId', + ]; + + if (!$row[$oldname] && !in_array($funcName, $nullableFuncs, true)) { throw new \InvalidArgumentException("Could not find value for {$table} {$oldname}"); } } diff --git a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php index b49592e97..2da281447 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php @@ -177,6 +177,36 @@ protected function mapExtendedType(array $row): string return Inflector::pluralize($bits[0]) . "." . $bits[1]; } + protected function mapHistoricPetitionViewerId(array $row): ?int + { + // 1) Flow id available directly on the row (co_petitions) + $flowId = null; + if (!empty($row['enrollment_flow_id'])) { + $flowId = (int)$row['enrollment_flow_id']; + } + + // 2) Otherwise resolve via parent Petition (co_petition_attributes) + if ($flowId === null && isset($row['petition_id'])) { + $petitionId = (int)$row['petition_id']; + if ($petitionId > 0) { + // Query inbound petitions table to get its enrollment flow id + $qualified = $this->inconn->qualifyTableName('cm_co_petitions'); + $sql = "SELECT co_enrollment_flow_id FROM {$qualified} WHERE id = ?"; + $flowId = (int)$this->inconn->fetchOne($sql, [$petitionId]) ?: null; + } + } + + if ($flowId === null) { + // Not resolvable; the target column is nullable + return null; + } + + // 3) Map flow -> viewer id via cache created when steps/viewer row were inserted + $viewerId = $this->cache['enrollment_flows']['id'][$flowId]['historic_petition_viewers']['id'] ?? null; + + return $viewerId !== null ? (int)$viewerId : null; + } + /** * Map identifier type to corresponding type ID *