Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public function dispatch(string $id) {
attributes: array_diff_key($this->request->getData(), ['petition_id' => true, 'token' => true])
);

$this->persistPetitionCouIdFromCollectedAttributes(
petitionId: (int)$petition->id,
attributeCollectorId: (int)$id
);


// On success, indicate the step is completed and generate a redirect
// to the next step

Expand Down Expand Up @@ -100,4 +106,72 @@ public function dispatch(string $id) {

$this->render('/Standard/dispatch');
}

/**
* If the Attribute Collector collected a COU for later context (eg approvals),
* persist Petition.cou_id during dispatch (not during finalization).
*
* This is scoped to the current Attribute Collector instance so that when multiple
* steps collect COU, each step updates Petition.cou_id to the value collected in
* that step (ie "last step wins" by virtue of being called last).
*
* @since COmanage Registry v5.2.0
* @param int $petitionId
* @param int $attributeCollectorId
* @return void
*/
protected function persistPetitionCouIdFromCollectedAttributes(
int $petitionId,
int $attributeCollectorId
): void {
$petitionAttributesTable = $this->AttributeCollectors
->EnrollmentAttributes
->PetitionAttributes;
$enrollmentAttributesTable = $this->AttributeCollectors
->EnrollmentAttributes;

// Find the EnrollmentAttribute id for cou_id configured on THIS Attribute Collector.
$couEnrollmentAttribute = $enrollmentAttributesTable
->find()
->select(['id'])
->where([
'EnrollmentAttributes.attribute_collector_id' => $attributeCollectorId,
'EnrollmentAttributes.attribute' => 'cou_id'
])
->first();

if (empty($couEnrollmentAttribute?->id)) {
return;
}

$couEnrollmentAttributeId = (int)$couEnrollmentAttribute->id;

// Find the most recently modified, non-empty PetitionAttribute for this EnrollmentAttribute ID.
$couAttr = $petitionAttributesTable
->find()
->where([
'PetitionAttributes.petition_id' => $petitionId,
'PetitionAttributes.enrollment_attribute_id' => $couEnrollmentAttributeId,
'PetitionAttributes.value IS NOT' => null,
'PetitionAttributes.value <>' => '',
])
->orderBy(['PetitionAttributes.modified' => 'DESC', 'PetitionAttributes.id' => 'DESC'])
->first();

if (!$couAttr) {
return;
}

$newCouId = (int)$couAttr->value;

$petitionsTable = $this->fetchTable('Petitions');
$petition = $petitionsTable->get($petitionId);

if ((int)($petition->cou_id ?? 0) === $newCouId) {
return;
}

$petition->cou_id = $newCouId;
$petitionsTable->saveOrFail($petition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

namespace CoreEnroller\Model\Table;

use Cake\Event\EventInterface;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use \App\Lib\Enum\GroupTypeEnum;
Expand Down Expand Up @@ -435,4 +435,149 @@ public function validationDefault(Validator $validator): Validator {

return $validator;
}

/**
* Application integrity rules.
*
* @since COmanage Registry v5.2.0
* @param RulesChecker $rules Rules checker
* @return RulesChecker
*/
public function buildRules(RulesChecker $rules): RulesChecker {

$rules->add(
function ($entity, array $options) {
return $this->ruleUniquePersonRoleAttributePerCollector($entity, $options);
},
'uniquePersonRoleAttributePerCollector',
[
'errorField' => 'attribute',
'message' => __d('error', 'exists', [__d('controller', 'EnrollmentAttributes', [1])]),
]
);

return $rules;
}

/**
* Enforce uniqueness constraints for EnrollmentAttribute definitions within a single
* Attribute Collector configuration.
*
* This rule is scoped to attribute_collector_id (ie, per Attribute Collector instance),
* so that different Enrollment Flow Steps may configure the same attribute in order to
* allow later actors/steps to review and modify values entered earlier.
*
* Rules enforced:
* - Single-valued PersonRole attributes (eg cou_id, affiliation_type_id, title, valid_from,
* valid_through, etc) may only be configured once per Attribute Collector.
* - PersonRole MVEAs:
* - address and telephoneNumber may repeat, but must be unique per attribute_type
* - adHocAttribute may repeat, but must be unique per (attribute_tag, label) pair,
* compared case-insensitively
*
* @since COmanage Registry v5.2.0
* @param \Cake\Datasource\EntityInterface $entity EnrollmentAttribute entity being saved.
* @param array<string, mixed> $options Options passed from the ORM save operation.
* @return bool|string True if the rule passes; otherwise an error message string.
*/
public function ruleUniquePersonRoleAttributePerCollector($entity, array $options): bool|string {
if (empty($entity->attribute_collector_id) || empty($entity->attribute)) {
return true;
}

$collectorId = (int)$entity->attribute_collector_id;
$attribute = (string)$entity->attribute;

$baseWhere = [
'attribute_collector_id' => $collectorId,
];

if (!empty($entity->id)) {
$baseWhere['id <>'] = (int)$entity->id;
}

// 1) Single-valued PersonRole model attributes: exactly one per Attribute Collector.
// These do NOT use attribute_mvea_parent.
$singleValuePersonRoleAttributes = [
'affiliation_type_id',
'cou_id',
'department',
'manager_person_id',
'organization',
'sponsor_person_id',
'title',
'valid_from',
'valid_through',
];

if (in_array($attribute, $singleValuePersonRoleAttributes, true)) {
if ($this->exists($baseWhere + ['attribute' => $attribute])) {
return __d('error', 'exists', [__d('controller', 'EnrollmentAttributes', [1])]);
}
return true;
}

// Everything below is about MVEAs attached to PersonRole.
$mveaParent = $entity->attribute_mvea_parent ?? null;
if ($mveaParent !== 'PersonRole') {
// Not a PersonRole MVEA config (could be Person MVEA, Petition, Group, etc)
return true;
}

$mveaBaseWhere = $baseWhere + [
'attribute' => $attribute,
'attribute_mvea_parent' => 'PersonRole',
];

// 2) Address + Telephone Number: can have multiple, but must differ by attribute_type.
if ($attribute === 'address' || $attribute === 'telephoneNumber') {
if (empty($entity->attribute_type)) {
return true; // let other validation complain about missing type if required
}

if ($this->exists($mveaBaseWhere + ['attribute_type' => (int)$entity->attribute_type])) {
return __d('error', 'exists', [__d('controller', 'EnrollmentAttributes', [1])]);
}

return true;
}

// 3) Ad Hoc Attribute: can have multiple, but must differ by (attribute_tag, label),
// compared case-insensitively.
if ($attribute === 'adHocAttribute') {
$tag = (string)($entity->attribute_tag ?? '');
$label = (string)($entity->label ?? '');

if (trim($tag) === '' || trim($label) === '') {
return true; // let other validation handle missing tag/label if needed
}

$tagLower = strtolower($tag);
$labelLower = strtolower($label);

$q = $this->find()
->where($mveaBaseWhere)
->where(function ($exp) use ($tagLower, $labelLower) {
return $exp
->eq('LOWER(attribute_tag)', $tagLower)
->eq('LOWER(label)', $labelLower);
});

if ($q->count() > 0) {
return __d('error', 'exists', [__d('controller', 'EnrollmentAttributes', [1])]);
}

return true;
}

// For any other PersonRole MVEAs not explicitly handled, keep a conservative uniqueness
// guard to prevent exact duplicates for the same attribute + label within the collector.
if (!empty($entity->label)) {
if ($this->exists($mveaBaseWhere + ['label' => (string)$entity->label])) {
return __d('error', 'exists', [__d('controller', 'EnrollmentAttributes', [1])]);
}
}

return true;
}
}