diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index ce17ffe5..ab55b0b6 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -162,7 +162,6 @@ "index_display": { "type": "boolean" }, "alphanumeric": { "type": "boolean" }, "case_sensitive": { "type": "boolean" }, - "invalidates": { "type": "boolean" }, "null_equivalents": { "type": "boolean" }, "search_distance": { "type": "integer" }, "search_exact": { "type": "boolean" }, diff --git a/app/resources/locales/en_US/default.po b/app/resources/locales/en_US/default.po index b3a9fc8a..4893d51d 100644 --- a/app/resources/locales/en_US/default.po +++ b/app/resources/locales/en_US/default.po @@ -260,6 +260,9 @@ msgstr "Exact" msgid "match.en.SearchTypeEnum.M" msgstr "Mapping" +msgid "match.en.SearchTypeEnum.IN" +msgstr "Invalidate" + msgid "match.en.SearchTypeEnum.X" msgstr "Skip" @@ -492,9 +495,6 @@ msgstr "ID" msgid "match.fd.index_display" msgstr "Display Field in Matchgrid Index" -msgid "match.fd.invalidates" -msgstr "Invalidates" - msgid "match.fd.label" msgstr "Label" diff --git a/app/src/Lib/Enum/SearchTypeEnum.php b/app/src/Lib/Enum/SearchTypeEnum.php index 5e97df6a..11f80299 100644 --- a/app/src/Lib/Enum/SearchTypeEnum.php +++ b/app/src/Lib/Enum/SearchTypeEnum.php @@ -30,9 +30,10 @@ namespace App\Lib\Enum; class SearchTypeEnum extends StandardEnum { - const Distance = 'D'; - const Exact = 'E'; - const Mapping = 'M'; - const Skip = 'X'; - const Substring = 'S'; + const Distance = 'D'; + const Exact = 'E'; + const Invalidate = "IN"; + const Mapping = 'M'; + const Skip = 'X'; + const Substring = 'S'; } \ No newline at end of file diff --git a/app/src/Lib/Match/MatchService.php b/app/src/Lib/Match/MatchService.php index 30ef9772..5928c32f 100644 --- a/app/src/Lib/Match/MatchService.php +++ b/app/src/Lib/Match/MatchService.php @@ -625,7 +625,9 @@ protected function search(string $mode, ]; foreach($rule->rule_attributes as $ruleattr) { - if($ruleattr->search_type == SearchTypeEnum::Skip) { + if($ruleattr->search_type == SearchTypeEnum::Skip + // We skip invalidate rules as well, these are handled in searchReferenceId() + || $ruleattr->search_type == SearchTypeEnum::Invalidate) { continue; } @@ -849,20 +851,80 @@ public function searchReferenceId(string $sor, string $sorid, AttributeManager $ skipSor: $trustMode == TrustModeEnum::Trust ); + // Was a canonical match downgraded? + $downgraded = false; + switch($canonicalMatches->count()) { case 1: + // Before returning this canonical match, we need to perform some checks that + // might demote the result to potential. Note that $results may have more than + // one row even for a canonical match, since there may be multiple matchgrid + // entries that point to the same Reference ID. + + $results = $canonicalMatches->getRawResults(); + + // First, check if any Attribute Rules in "Invalidate" mode will demote this result. + + foreach($results as $rowId => $attrs) { + // Find the rule that generated this match. + + $rulename = $canonicalMatches->getRuleForResult($rowId); + + foreach($this->mgConfig->canonical_rules as $rule) { + if($rule->name == $rulename) { + // This is the correct rule, check the Rule Attributes. Note there could + // be more than one Rule Attribute with Search Type "Invalidate". + + foreach($rule->rule_attributes as $ruleAttribute) { + if($ruleAttribute->search_type == SearchTypeEnum::Invalidate) { + // The name of the attribute for this Rule Attribute, eg "dob". + // This is the column name, not the API name. + $ruleAttributeName = $ruleAttribute->attribute->name; + + // The value in the search request + $searchValue = $attributes->getValueByAttribute($ruleAttribute->attribute); + // The value in the matched row + $rowValue = $results[$rowId][$ruleAttributeName]; + + // We only perform the validation check if both values are not empty. + if(!empty($searchValue) + && !empty($rowValue) + && ($searchValue !== $rowValue)) { + // The returned value does not match, invalidate the request and drop + // the result to potential + + Log::write('debug', $sor . "/" . $sorid . " Invalidate mode for $ruleAttributeName ($rulename) downgrading result to Potential"); + + $downgraded = [ + 'rule' => "_invalidate", + 'attrs' => $attrs + ]; + + // No need to continue with any loop + break 3; + } + } + } + + // We found the rule we're looking for, no need to continue with the loop + break; + } + } + + // If we have multiple rows, we should have the same rule for each row, + // so we can stop after the first. + break; + } + + if(!$downgraded) { + // Next we need to handle Potential Trust Mode, check to see if any result row + // matched the SOR. + if($trustMode != TrustModeEnum::Potential) { // Exact match, return return $canonicalMatches; } - // Handling Potential mode is a bit more complicated. First, get the - // raw results. Even though count() was 1, $results may have more than - // one entry since we're getting the per-row matches. Check to see if - // _any_ row matched the SOR. - - $results = $canonicalMatches->getRawResults(); - foreach($results as $rowId => $attrs) { if($attrs['sor'] == $sor) { $sorMatch = $attrs; @@ -876,8 +938,12 @@ public function searchReferenceId(string $sor, string $sorid, AttributeManager $ return $canonicalMatches; } + $downgraded = [ + 'rule' => "_trustmode", + 'attrs' => $sorMatch + ]; Log::write('debug', $sor . "/" . $sorid . " Trust Mode downgrading result to Potential"); - + } break; case 0: // Fall through and try potential matches @@ -900,11 +966,11 @@ public function searchReferenceId(string $sor, string $sorid, AttributeManager $ skipSor: $trustMode == TrustModeEnum::Trust ); - // Add in the potential match from above, if configured + // Add in the downgraded potential match from above, if configured - if($sorMatch) { + if($downgraded !== false) { // We use a psuedo-rule name - $potentialMatches->add($sorMatch, "_trustmode"); + $potentialMatches->add($downgraded['attrs'], $downgraded['rule']); } // The calling code generally checks to see if any rules successfully ran, diff --git a/app/src/Lib/Match/ResultManager.php b/app/src/Lib/Match/ResultManager.php index 00a70979..313e15cc 100644 --- a/app/src/Lib/Match/ResultManager.php +++ b/app/src/Lib/Match/ResultManager.php @@ -39,12 +39,16 @@ class ResultManager { // for the same referenceId). Attributes are stored in the same format as AttributeManager // to faciliate wire representation. protected $results = []; + // Track which rule created which results. This will only be populated for rules + // that generated results. + protected $resultRules = []; // Keep the raw results as well, to facilitate UI representation. protected $rawResults = []; // "canonical" or "potential" protected $confidenceMode = null; protected $attrconfig = []; - // We track which rules ran successfully as a way of validating the inbound request + // We track which rules ran successfully as a way of validating the inbound request. + // This includes rules that did not generate any results. protected $successfulRules = []; /** @@ -97,6 +101,8 @@ public function add(array $attributes, string $rule=null) { } $this->results[$referenceId][$rowId] = $parsed; + + $this->resultRules[$rowId] = $rule; $this->rawResults[$rowId] = $attributes; } @@ -274,6 +280,18 @@ public function getResultsForJson($mode="search") { return $ret; } + /** + * Get the name of the rule that generated the result for the specified row ID. + * + * @since COmanage Match v1.2.0 + * @param int $rowId Row to return rule for + * @return string Rule name (if found) or false + */ + + public function getRuleForResult(int $rowId) { + return $this->resultRules[$rowId] ?? false; + } + /** * Obtain an array of successfully executed rules as reported to the ResultManager. * Note that successful rules may have generated no matches. diff --git a/app/src/Model/Table/RuleAttributesTable.php b/app/src/Model/Table/RuleAttributesTable.php index 03c6f828..9f671f44 100644 --- a/app/src/Model/Table/RuleAttributesTable.php +++ b/app/src/Model/Table/RuleAttributesTable.php @@ -122,6 +122,7 @@ public function validationDefault(Validator $validator): Validator { [ 'rule' => [ 'inList', [ SearchTypeEnum::Distance, SearchTypeEnum::Exact, + SearchTypeEnum::Invalidate, SearchTypeEnum::Mapping, SearchTypeEnum::Skip, SearchTypeEnum::Substring diff --git a/app/templates/Attributes/fields.inc b/app/templates/Attributes/fields.inc index e412c5c6..9fb5c8f3 100644 --- a/app/templates/Attributes/fields.inc +++ b/app/templates/Attributes/fields.inc @@ -37,8 +37,6 @@ if($action == 'add' || $action == 'edit') { print $this->Field->control('alphanumeric', [], false); print $this->Field->control('case_sensitive', [], false); -// CO-1762 -// print $this->Field->control('invalidates', [], false); print $this->Field->control('null_equivalents', [], false); print $this->Field->control('search_distance', [], false);