--- /dev/null
+<?php
+namespace Civi\Afform\behavior;
+
+use Civi\Afform\AbstractBehavior;
+use Civi\Afform\Event\AfformSubmitEvent;
+use Civi\Api4\Contact;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use CRM_Afform_ExtensionUtil as E;
+
+class ContactDedupe extends AbstractBehavior implements EventSubscriberInterface {
+
+ /**
+ * @return array
+ */
+ public static function getSubscribedEvents() {
+ return [
+ 'civi.afform.submit' => ['onAfformSubmit', 101],
+ ];
+ }
+
+ public static function getEntities():array {
+ return \CRM_Contact_BAO_ContactType::basicTypes();
+ }
+
+ public static function getTitle():string {
+ return E::ts('Duplicate Matching');
+ }
+
+ public static function getDescription():string {
+ return E::ts('Update existing contact instead of creating a new one based on a dedupe rule.');
+ }
+
+ public static function getModes(string $entityName):array {
+ $modes = [];
+ $dedupeRuleGroups = \Civi\Api4\DedupeRuleGroup::get(FALSE)
+ ->addWhere('contact_type', '=', $entityName)
+ ->addOrderBy('used', 'DESC')
+ ->addOrderBy('title', 'ASC')
+ ->execute();
+ foreach ($dedupeRuleGroups as $rule) {
+ // Use the generic API name for supervised/unsupervised rules as it's more portable
+ $name = ($rule['used'] === 'General' ? $rule['name'] : $entityName . '.' . $rule['used']);
+ $modes[] = [
+ 'name' => $name,
+ 'label' => $rule['title'],
+ ];
+ }
+ return $modes;
+ }
+
+ public static function onAfformSubmit(AfformSubmitEvent $event) {
+ $entity = $event->getEntity();
+ $dedupeMode = $entity['contact-dedupe'] ?? NULL;
+ if ($event->getEntityType() !== 'Contact' || !$dedupeMode) {
+ return;
+ }
+ // Apply dedupe rule if contact isn't already identified
+ foreach ($event->records as $index => $record) {
+ $supportedJoins = ['Address', 'Email', 'Phone', 'IM'];
+ $values = $record['fields'] ?? [];
+ foreach ($supportedJoins as $joinEntity) {
+ if (!empty($record['joins'][$joinEntity][0])) {
+ $values += \CRM_Utils_Array::prefixKeys($record['joins'][$joinEntity][0], strtolower($joinEntity) . '_primary.');
+ }
+ }
+ $match = Contact::getDuplicates(FALSE)
+ ->setValues($values)
+ ->setDedupeRule($dedupeMode)
+ ->execute()->first();
+ if (!empty($match['id'])) {
+ $event->setEntityId($index, $match['id']);
+ }
+ }
+ }
+
+}
$this->assertEquals($locationType, $submission['data']['Organization1'][0]['_joins']['Email'][0]['location_type_id']);
}
+ public function testDedupeIndividual(): void {
+ $layout = <<<EOHTML
+<af-form ctrl="modelListCtrl">
+ <af-entity type="Contact" data="{contact_type: 'Individual'}" name="Individual1" contact-dedupe="Individual.Supervised" />
+ <fieldset af-fieldset="Individual1">
+ <af-field name="first_name" />
+ <af-field name="middle_name" />
+ <af-field name="last_name" />
+ <div af-join="Email" min="1" af-repeat="Add">
+ <afblock-contact-email></afblock-contact-email>
+ </div>
+ </fieldset>
+</af-form>
+EOHTML;
+ $this->useValues([
+ 'layout' => $layout,
+ 'permission' => CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION,
+ ]);
+
+ $lastName = uniqid(__FUNCTION__);
+ $contact = \Civi\Api4\Contact::create(FALSE)
+ ->addValue('first_name', 'Bob')
+ ->addValue('last_name', $lastName)
+ ->addValue('email_primary.email', '123@example.com')
+ ->execute()->single();
+
+ $locationType = CRM_Core_BAO_LocationType::getDefault()->id;
+ $values = [
+ 'Individual1' => [
+ [
+ 'fields' => [
+ 'first_name' => 'Bob',
+ 'middle_name' => 'New',
+ 'last_name' => $lastName,
+ ],
+ 'joins' => [
+ 'Email' => [
+ ['email' => '123@example.com', 'location_type_id' => $locationType],
+ ],
+ ],
+ ],
+ ],
+ ];
+ Civi\Api4\Afform::submit()
+ ->setName($this->formName)
+ ->setValues($values)
+ ->execute();
+
+ // Check that the contact was updated per dedupe rule
+ $result = \Civi\Api4\Contact::get(FALSE)
+ ->addWhere('id', '=', $contact['id'])
+ ->execute()->single();
+ $this->assertEquals('New', $result['middle_name']);
+ }
+
}